Merge branch 'beta' into map_editor_open_recent-v2
6
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -15,6 +15,7 @@ Please attach game logs: `VCMI_client.txt`, `VCMI_server.txt` etc.
|
|||||||
|
|
||||||
**To Reproduce**
|
**To Reproduce**
|
||||||
Steps to reproduce the behavior:
|
Steps to reproduce the behavior:
|
||||||
|
|
||||||
1. Go to '...'
|
1. Go to '...'
|
||||||
2. Click on '....'
|
2. Click on '....'
|
||||||
3. Scroll down to '....'
|
3. Scroll down to '....'
|
||||||
@ -33,8 +34,9 @@ If this something which worked well some time ago, please let us know about vers
|
|||||||
If applicable, add screenshots to help explain your problem.
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
**Version**
|
**Version**
|
||||||
- OS: [e.g. Windows, macOS Intel, macOS ARM, Android, Linux, iOS]
|
|
||||||
- Version: [VCMI version]
|
- OS: [e.g. Windows, macOS Intel, macOS ARM, Android, Linux, iOS]
|
||||||
|
- Version: [VCMI version]
|
||||||
|
|
||||||
**Additional context**
|
**Additional context**
|
||||||
Add any other context about the problem here.
|
Add any other context about the problem here.
|
||||||
|
6
.github/workflows/github.yml
vendored
@ -402,3 +402,9 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
sudo apt install python3-jstyleson
|
sudo apt install python3-jstyleson
|
||||||
python3 CI/validate_json.py
|
python3 CI/validate_json.py
|
||||||
|
|
||||||
|
- name: Validate Markdown
|
||||||
|
uses: DavidAnson/markdownlint-cli2-action@v18
|
||||||
|
with:
|
||||||
|
config: 'CI/example.markdownlint-cli2.jsonc'
|
||||||
|
globs: '**/*.md'
|
||||||
|
@ -675,7 +675,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
|
|||||||
spells::BattleCast cast(state.get(), hero, spells::Mode::HERO, ps.spell);
|
spells::BattleCast cast(state.get(), hero, spells::Mode::HERO, ps.spell);
|
||||||
cast.castEval(state->getServerCallback(), ps.dest);
|
cast.castEval(state->getServerCallback(), ps.dest);
|
||||||
|
|
||||||
auto allUnits = state->battleGetUnitsIf([](const battle::Unit * u) -> bool { return true; });
|
auto allUnits = state->battleGetUnitsIf([](const battle::Unit * u) -> bool { return u->isValidTarget(); });
|
||||||
|
|
||||||
auto needFullEval = vstd::contains_if(allUnits, [&](const battle::Unit * u) -> bool
|
auto needFullEval = vstd::contains_if(allUnits, [&](const battle::Unit * u) -> bool
|
||||||
{
|
{
|
||||||
@ -731,7 +731,6 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
|
|||||||
|
|
||||||
ps.value = scoreEvaluator.evaluateExchange(updatedAttack, cachedAttack.turn, *targets, innerCache, state);
|
ps.value = scoreEvaluator.evaluateExchange(updatedAttack, cachedAttack.turn, *targets, innerCache, state);
|
||||||
}
|
}
|
||||||
|
|
||||||
for(const auto & unit : allUnits)
|
for(const auto & unit : allUnits)
|
||||||
{
|
{
|
||||||
if(!unit->isValidTarget(true))
|
if(!unit->isValidTarget(true))
|
||||||
@ -771,11 +770,31 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
|
|||||||
ps.value -= 4 * dpsReduce * scoreEvaluator.getNegativeEffectMultiplier();
|
ps.value -= 4 * dpsReduce * scoreEvaluator.getNegativeEffectMultiplier();
|
||||||
|
|
||||||
#if BATTLE_TRACE_LEVEL >= 1
|
#if BATTLE_TRACE_LEVEL >= 1
|
||||||
logAi->trace(
|
// Ensure ps.dest is not empty before accessing the first element
|
||||||
"Spell affects %s (%d), dps: %2f",
|
if (!ps.dest.empty())
|
||||||
unit->creatureId().toCreature()->getNameSingularTranslated(),
|
{
|
||||||
unit->getCount(),
|
logAi->trace(
|
||||||
dpsReduce);
|
"Spell %s to %d affects %s (%d), dps: %2f oldHealth: %d newHealth: %d",
|
||||||
|
ps.spell->getNameTranslated(),
|
||||||
|
ps.dest.at(0).hexValue.hex, // Safe to access .at(0) now
|
||||||
|
unit->creatureId().toCreature()->getNameSingularTranslated(),
|
||||||
|
unit->getCount(),
|
||||||
|
dpsReduce,
|
||||||
|
oldHealth,
|
||||||
|
newHealth);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Handle the case where ps.dest is empty
|
||||||
|
logAi->trace(
|
||||||
|
"Spell %s has no destination, affects %s (%d), dps: %2f oldHealth: %d newHealth: %d",
|
||||||
|
ps.spell->getNameTranslated(),
|
||||||
|
unit->creatureId().toCreature()->getNameSingularTranslated(),
|
||||||
|
unit->getCount(),
|
||||||
|
dpsReduce,
|
||||||
|
oldHealth,
|
||||||
|
newHealth);
|
||||||
|
}
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -906,7 +906,7 @@ std::vector<const battle::Unit *> BattleExchangeEvaluator::getOneTurnReachableUn
|
|||||||
{
|
{
|
||||||
std::vector<const battle::Unit *> result;
|
std::vector<const battle::Unit *> result;
|
||||||
|
|
||||||
for(int i = 0; i < turnOrder.size(); i++)
|
for(int i = 0; i < turnOrder.size(); i++, turn++)
|
||||||
{
|
{
|
||||||
auto & turnQueue = turnOrder[i];
|
auto & turnQueue = turnOrder[i];
|
||||||
HypotheticBattle turnBattle(env.get(), cb);
|
HypotheticBattle turnBattle(env.get(), cb);
|
||||||
|
@ -34,11 +34,6 @@
|
|||||||
namespace NKAI
|
namespace NKAI
|
||||||
{
|
{
|
||||||
|
|
||||||
// our to enemy strength ratio constants
|
|
||||||
const float SAFE_ATTACK_CONSTANT = 1.1f;
|
|
||||||
const float RETREAT_THRESHOLD = 0.3f;
|
|
||||||
const double RETREAT_ABSOLUTE_THRESHOLD = 10000.;
|
|
||||||
|
|
||||||
//one thread may be turn of AI and another will be handling a side effect for AI2
|
//one thread may be turn of AI and another will be handling a side effect for AI2
|
||||||
thread_local CCallback * cb = nullptr;
|
thread_local CCallback * cb = nullptr;
|
||||||
thread_local AIGateway * ai = nullptr;
|
thread_local AIGateway * ai = nullptr;
|
||||||
@ -286,6 +281,9 @@ void AIGateway::tileRevealed(const std::unordered_set<int3> & pos)
|
|||||||
for(const CGObjectInstance * obj : myCb->getVisitableObjs(tile))
|
for(const CGObjectInstance * obj : myCb->getVisitableObjs(tile))
|
||||||
addVisitableObj(obj);
|
addVisitableObj(obj);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (nullkiller->settings->isUpdateHitmapOnTileReveal())
|
||||||
|
nullkiller->dangerHitMap->reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
void AIGateway::heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query)
|
void AIGateway::heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query)
|
||||||
@ -553,7 +551,7 @@ std::optional<BattleAction> AIGateway::makeSurrenderRetreatDecision(const Battle
|
|||||||
double fightRatio = ourStrength / (double)battleState.getEnemyStrength();
|
double fightRatio = ourStrength / (double)battleState.getEnemyStrength();
|
||||||
|
|
||||||
// if we have no towns - things are already bad, so retreat is not an option.
|
// if we have no towns - things are already bad, so retreat is not an option.
|
||||||
if(cb->getTownsInfo().size() && ourStrength < RETREAT_ABSOLUTE_THRESHOLD && fightRatio < RETREAT_THRESHOLD && battleState.canFlee)
|
if(cb->getTownsInfo().size() && ourStrength < nullkiller->settings->getRetreatThresholdAbsolute() && fightRatio < nullkiller->settings->getRetreatThresholdRelative() && battleState.canFlee)
|
||||||
{
|
{
|
||||||
return BattleAction::makeRetreat(battleState.ourSide);
|
return BattleAction::makeRetreat(battleState.ourSide);
|
||||||
}
|
}
|
||||||
@ -670,7 +668,7 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector<C
|
|||||||
else if(objType == Obj::ARTIFACT || objType == Obj::RESOURCE)
|
else if(objType == Obj::ARTIFACT || objType == Obj::RESOURCE)
|
||||||
{
|
{
|
||||||
bool dangerUnknown = danger == 0;
|
bool dangerUnknown = danger == 0;
|
||||||
bool dangerTooHigh = ratio > (1 / SAFE_ATTACK_CONSTANT);
|
bool dangerTooHigh = ratio * nullkiller->settings->getSafeAttackRatio() > 1;
|
||||||
|
|
||||||
answer = !dangerUnknown && !dangerTooHigh;
|
answer = !dangerUnknown && !dangerTooHigh;
|
||||||
}
|
}
|
||||||
|
@ -146,21 +146,21 @@ bool HeroPtr::operator==(const HeroPtr & rhs) const
|
|||||||
return h == rhs.get(true);
|
return h == rhs.get(true);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet * heroArmy, uint64_t dangerStrength)
|
bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet * heroArmy, uint64_t dangerStrength, float safeAttackRatio)
|
||||||
{
|
{
|
||||||
const ui64 heroStrength = h->getFightingStrength() * heroArmy->getArmyStrength();
|
const ui64 heroStrength = h->getHeroStrength() * heroArmy->getArmyStrength();
|
||||||
|
|
||||||
if(dangerStrength)
|
if(dangerStrength)
|
||||||
{
|
{
|
||||||
return heroStrength / SAFE_ATTACK_CONSTANT > dangerStrength;
|
return heroStrength > dangerStrength * safeAttackRatio;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true; //there's no danger
|
return true; //there's no danger
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength)
|
bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength, float safeAttackRatio)
|
||||||
{
|
{
|
||||||
return isSafeToVisit(h, h, dangerStrength);
|
return isSafeToVisit(h, h, dangerStrength, safeAttackRatio);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isObjectRemovable(const CGObjectInstance * obj)
|
bool isObjectRemovable(const CGObjectInstance * obj)
|
||||||
|
@ -61,11 +61,6 @@ const int GOLD_MINE_PRODUCTION = 1000;
|
|||||||
const int WOOD_ORE_MINE_PRODUCTION = 2;
|
const int WOOD_ORE_MINE_PRODUCTION = 2;
|
||||||
const int RESOURCE_MINE_PRODUCTION = 1;
|
const int RESOURCE_MINE_PRODUCTION = 1;
|
||||||
const int ACTUAL_RESOURCE_COUNT = 7;
|
const int ACTUAL_RESOURCE_COUNT = 7;
|
||||||
const int ALLOWED_ROAMING_HEROES = 8;
|
|
||||||
|
|
||||||
//implementation-dependent
|
|
||||||
extern const float SAFE_ATTACK_CONSTANT;
|
|
||||||
extern const int GOLD_RESERVE;
|
|
||||||
|
|
||||||
extern thread_local CCallback * cb;
|
extern thread_local CCallback * cb;
|
||||||
|
|
||||||
@ -213,8 +208,8 @@ bool isBlockVisitObj(const int3 & pos);
|
|||||||
bool isWeeklyRevisitable(const Nullkiller * ai, const CGObjectInstance * obj);
|
bool isWeeklyRevisitable(const Nullkiller * ai, const CGObjectInstance * obj);
|
||||||
|
|
||||||
bool isObjectRemovable(const CGObjectInstance * obj); //FIXME FIXME: move logic to object property!
|
bool isObjectRemovable(const CGObjectInstance * obj); //FIXME FIXME: move logic to object property!
|
||||||
bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength);
|
bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength, float safeAttackRatio);
|
||||||
bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet *, uint64_t dangerStrength);
|
bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet *, uint64_t dangerStrength, float safeAttackRatio);
|
||||||
|
|
||||||
bool compareHeroStrength(const CGHeroInstance * h1, const CGHeroInstance * h2);
|
bool compareHeroStrength(const CGHeroInstance * h1, const CGHeroInstance * h2);
|
||||||
bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2);
|
bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2);
|
||||||
|
@ -13,6 +13,7 @@
|
|||||||
#include "../Engine/Nullkiller.h"
|
#include "../Engine/Nullkiller.h"
|
||||||
#include "../../../CCallback.h"
|
#include "../../../CCallback.h"
|
||||||
#include "../../../lib/mapObjects/MapObjects.h"
|
#include "../../../lib/mapObjects/MapObjects.h"
|
||||||
|
#include "../../../lib/IGameSettings.h"
|
||||||
#include "../../../lib/GameConstants.h"
|
#include "../../../lib/GameConstants.h"
|
||||||
|
|
||||||
namespace NKAI
|
namespace NKAI
|
||||||
@ -152,16 +153,6 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
|
|||||||
uint64_t armyValue = 0;
|
uint64_t armyValue = 0;
|
||||||
|
|
||||||
TemporaryArmy newArmyInstance;
|
TemporaryArmy newArmyInstance;
|
||||||
auto bonusModifiers = armyCarrier->getBonuses(Selector::type()(BonusType::MORALE));
|
|
||||||
|
|
||||||
for(auto bonus : *bonusModifiers)
|
|
||||||
{
|
|
||||||
// army bonuses will change and object bonuses are temporary
|
|
||||||
if(bonus->source != BonusSource::ARMY && bonus->source != BonusSource::OBJECT_INSTANCE && bonus->source != BonusSource::OBJECT_TYPE)
|
|
||||||
{
|
|
||||||
newArmyInstance.addNewBonus(std::make_shared<Bonus>(*bonus));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
while(allowedFactions.size() < alignmentMap.size())
|
while(allowedFactions.size() < alignmentMap.size())
|
||||||
{
|
{
|
||||||
@ -197,16 +188,18 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
|
|||||||
auto morale = slot.second->moraleVal();
|
auto morale = slot.second->moraleVal();
|
||||||
auto multiplier = 1.0f;
|
auto multiplier = 1.0f;
|
||||||
|
|
||||||
const float BadMoraleChance = 0.083f;
|
const auto & badMoraleDice = cb->getSettings().getVector(EGameSettings::COMBAT_BAD_MORALE_DICE);
|
||||||
const float HighMoraleChance = 0.04f;
|
const auto & highMoraleDice = cb->getSettings().getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE);
|
||||||
|
|
||||||
if(morale < 0)
|
if(morale < 0 && !badMoraleDice.empty())
|
||||||
{
|
{
|
||||||
multiplier += morale * BadMoraleChance;
|
size_t diceIndex = std::min<size_t>(badMoraleDice.size(), -morale) - 1;
|
||||||
|
multiplier -= 1.0 / badMoraleDice.at(diceIndex);
|
||||||
}
|
}
|
||||||
else if(morale > 0)
|
else if(morale > 0 && !highMoraleDice.empty())
|
||||||
{
|
{
|
||||||
multiplier += morale * HighMoraleChance;
|
size_t diceIndex = std::min<size_t>(highMoraleDice.size(), morale) - 1;
|
||||||
|
multiplier += 1.0 / highMoraleDice.at(diceIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
newValue += multiplier * slot.second->getPower();
|
newValue += multiplier * slot.second->getPower();
|
||||||
|
@ -39,7 +39,6 @@ void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo)
|
|||||||
for(int upgradeIndex : {1, 0})
|
for(int upgradeIndex : {1, 0})
|
||||||
{
|
{
|
||||||
BuildingID building = BuildingID(BuildingID::getDwellingFromLevel(level, upgradeIndex));
|
BuildingID building = BuildingID(BuildingID::getDwellingFromLevel(level, upgradeIndex));
|
||||||
|
|
||||||
if(!vstd::contains(buildings, building))
|
if(!vstd::contains(buildings, building))
|
||||||
continue; // no such building in town
|
continue; // no such building in town
|
||||||
|
|
||||||
@ -73,11 +72,18 @@ void BuildAnalyzer::updateOtherBuildings(TownDevelopmentInfo & developmentInfo)
|
|||||||
|
|
||||||
if(developmentInfo.existingDwellings.size() >= 2 && ai->cb->getDate(Date::DAY_OF_WEEK) > boost::date_time::Friday)
|
if(developmentInfo.existingDwellings.size() >= 2 && ai->cb->getDate(Date::DAY_OF_WEEK) > boost::date_time::Friday)
|
||||||
{
|
{
|
||||||
otherBuildings.push_back({BuildingID::CITADEL, BuildingID::CASTLE});
|
|
||||||
otherBuildings.push_back({BuildingID::HORDE_1});
|
otherBuildings.push_back({BuildingID::HORDE_1});
|
||||||
otherBuildings.push_back({BuildingID::HORDE_2});
|
otherBuildings.push_back({BuildingID::HORDE_2});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
otherBuildings.push_back({ BuildingID::CITADEL, BuildingID::CASTLE });
|
||||||
|
otherBuildings.push_back({ BuildingID::RESOURCE_SILO });
|
||||||
|
otherBuildings.push_back({ BuildingID::SPECIAL_1 });
|
||||||
|
otherBuildings.push_back({ BuildingID::SPECIAL_2 });
|
||||||
|
otherBuildings.push_back({ BuildingID::SPECIAL_3 });
|
||||||
|
otherBuildings.push_back({ BuildingID::SPECIAL_4 });
|
||||||
|
otherBuildings.push_back({ BuildingID::MARKETPLACE });
|
||||||
|
|
||||||
for(auto & buildingSet : otherBuildings)
|
for(auto & buildingSet : otherBuildings)
|
||||||
{
|
{
|
||||||
for(auto & buildingID : buildingSet)
|
for(auto & buildingID : buildingSet)
|
||||||
@ -141,6 +147,8 @@ void BuildAnalyzer::update()
|
|||||||
|
|
||||||
auto towns = ai->cb->getTownsInfo();
|
auto towns = ai->cb->getTownsInfo();
|
||||||
|
|
||||||
|
float economyDevelopmentCost = 0;
|
||||||
|
|
||||||
for(const CGTownInstance* town : towns)
|
for(const CGTownInstance* town : towns)
|
||||||
{
|
{
|
||||||
logAi->trace("Checking town %s", town->getNameTranslated());
|
logAi->trace("Checking town %s", town->getNameTranslated());
|
||||||
@ -153,6 +161,11 @@ void BuildAnalyzer::update()
|
|||||||
|
|
||||||
requiredResources += developmentInfo.requiredResources;
|
requiredResources += developmentInfo.requiredResources;
|
||||||
totalDevelopmentCost += developmentInfo.townDevelopmentCost;
|
totalDevelopmentCost += developmentInfo.townDevelopmentCost;
|
||||||
|
for(auto building : developmentInfo.toBuild)
|
||||||
|
{
|
||||||
|
if (building.dailyIncome[EGameResID::GOLD] > 0)
|
||||||
|
economyDevelopmentCost += building.buildCostWithPrerequisites[EGameResID::GOLD];
|
||||||
|
}
|
||||||
armyCost += developmentInfo.armyCost;
|
armyCost += developmentInfo.armyCost;
|
||||||
|
|
||||||
for(auto bi : developmentInfo.toBuild)
|
for(auto bi : developmentInfo.toBuild)
|
||||||
@ -171,15 +184,7 @@ void BuildAnalyzer::update()
|
|||||||
|
|
||||||
updateDailyIncome();
|
updateDailyIncome();
|
||||||
|
|
||||||
if(ai->cb->getDate(Date::DAY) == 1)
|
goldPressure = (ai->getLockedResources()[EGameResID::GOLD] + (float)armyCost[EGameResID::GOLD] + economyDevelopmentCost) / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f);
|
||||||
{
|
|
||||||
goldPressure = 1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
goldPressure = ai->getLockedResources()[EGameResID::GOLD] / 5000.0f
|
|
||||||
+ (float)armyCost[EGameResID::GOLD] / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f);
|
|
||||||
}
|
|
||||||
|
|
||||||
logAi->trace("Gold pressure: %f", goldPressure);
|
logAi->trace("Gold pressure: %f", goldPressure);
|
||||||
}
|
}
|
||||||
@ -237,6 +242,12 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
|
|||||||
logAi->trace("checking %s", info.name);
|
logAi->trace("checking %s", info.name);
|
||||||
logAi->trace("buildInfo %s", info.toString());
|
logAi->trace("buildInfo %s", info.toString());
|
||||||
|
|
||||||
|
int highestFort = 0;
|
||||||
|
for (auto twn : ai->cb->getTownsInfo())
|
||||||
|
{
|
||||||
|
highestFort = std::max(highestFort, (int)twn->fortLevel());
|
||||||
|
}
|
||||||
|
|
||||||
if(!town->hasBuilt(building))
|
if(!town->hasBuilt(building))
|
||||||
{
|
{
|
||||||
auto canBuild = ai->cb->canBuildStructure(town, building);
|
auto canBuild = ai->cb->canBuildStructure(town, building);
|
||||||
@ -281,7 +292,15 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
|
|||||||
prerequisite.baseCreatureID = info.baseCreatureID;
|
prerequisite.baseCreatureID = info.baseCreatureID;
|
||||||
prerequisite.prerequisitesCount++;
|
prerequisite.prerequisitesCount++;
|
||||||
prerequisite.armyCost = info.armyCost;
|
prerequisite.armyCost = info.armyCost;
|
||||||
prerequisite.dailyIncome = info.dailyIncome;
|
bool haveSameOrBetterFort = false;
|
||||||
|
if (prerequisite.id == BuildingID::FORT && highestFort >= CGTownInstance::EFortLevel::FORT)
|
||||||
|
haveSameOrBetterFort = true;
|
||||||
|
if (prerequisite.id == BuildingID::CITADEL && highestFort >= CGTownInstance::EFortLevel::CITADEL)
|
||||||
|
haveSameOrBetterFort = true;
|
||||||
|
if (prerequisite.id == BuildingID::CASTLE && highestFort >= CGTownInstance::EFortLevel::CASTLE)
|
||||||
|
haveSameOrBetterFort = true;
|
||||||
|
if(!haveSameOrBetterFort)
|
||||||
|
prerequisite.dailyIncome = info.dailyIncome;
|
||||||
|
|
||||||
return prerequisite;
|
return prerequisite;
|
||||||
}
|
}
|
||||||
|
@ -89,7 +89,6 @@ void DangerHitMapAnalyzer::updateHitMap()
|
|||||||
|
|
||||||
heroes[hero->tempOwner][hero] = HeroRole::MAIN;
|
heroes[hero->tempOwner][hero] = HeroRole::MAIN;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(obj->ID == Obj::TOWN)
|
if(obj->ID == Obj::TOWN)
|
||||||
{
|
{
|
||||||
auto town = dynamic_cast<const CGTownInstance *>(obj);
|
auto town = dynamic_cast<const CGTownInstance *>(obj);
|
||||||
@ -140,6 +139,7 @@ void DangerHitMapAnalyzer::updateHitMap()
|
|||||||
|
|
||||||
newThreat.hero = path.targetHero;
|
newThreat.hero = path.targetHero;
|
||||||
newThreat.turn = path.turn();
|
newThreat.turn = path.turn();
|
||||||
|
newThreat.threat = path.getHeroStrength() * (1 - path.movementCost() / 2.0);
|
||||||
newThreat.danger = path.getHeroStrength();
|
newThreat.danger = path.getHeroStrength();
|
||||||
|
|
||||||
if(newThreat.value() > node.maximumDanger.value())
|
if(newThreat.value() > node.maximumDanger.value())
|
||||||
@ -316,8 +316,8 @@ uint64_t DangerHitMapAnalyzer::enemyCanKillOurHeroesAlongThePath(const AIPath &
|
|||||||
|
|
||||||
const auto& info = getTileThreat(tile);
|
const auto& info = getTileThreat(tile);
|
||||||
|
|
||||||
return (info.fastestDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.fastestDanger.danger))
|
return (info.fastestDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.fastestDanger.danger, ai->settings->getSafeAttackRatio()))
|
||||||
|| (info.maximumDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.maximumDanger.danger));
|
|| (info.maximumDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.maximumDanger.danger, ai->settings->getSafeAttackRatio()));
|
||||||
}
|
}
|
||||||
|
|
||||||
const HitMapNode & DangerHitMapAnalyzer::getObjectThreat(const CGObjectInstance * obj) const
|
const HitMapNode & DangerHitMapAnalyzer::getObjectThreat(const CGObjectInstance * obj) const
|
||||||
@ -348,6 +348,7 @@ std::set<const CGObjectInstance *> DangerHitMapAnalyzer::getOneTurnAccessibleObj
|
|||||||
void DangerHitMapAnalyzer::reset()
|
void DangerHitMapAnalyzer::reset()
|
||||||
{
|
{
|
||||||
hitMapUpToDate = false;
|
hitMapUpToDate = false;
|
||||||
|
tileOwnersUpToDate = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -22,6 +22,7 @@ struct HitMapInfo
|
|||||||
|
|
||||||
uint64_t danger;
|
uint64_t danger;
|
||||||
uint8_t turn;
|
uint8_t turn;
|
||||||
|
float threat;
|
||||||
HeroPtr hero;
|
HeroPtr hero;
|
||||||
|
|
||||||
HitMapInfo()
|
HitMapInfo()
|
||||||
@ -33,6 +34,7 @@ struct HitMapInfo
|
|||||||
{
|
{
|
||||||
danger = 0;
|
danger = 0;
|
||||||
turn = 255;
|
turn = 255;
|
||||||
|
threat = 0;
|
||||||
hero = HeroPtr();
|
hero = HeroPtr();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -95,7 +95,7 @@ float HeroManager::evaluateSpeciality(const CGHeroInstance * hero) const
|
|||||||
|
|
||||||
float HeroManager::evaluateFightingStrength(const CGHeroInstance * hero) const
|
float HeroManager::evaluateFightingStrength(const CGHeroInstance * hero) const
|
||||||
{
|
{
|
||||||
return evaluateSpeciality(hero) + wariorSkillsScores.evaluateSecSkills(hero) + hero->level * 1.5f;
|
return evaluateSpeciality(hero) + wariorSkillsScores.evaluateSecSkills(hero) + hero->getBasePrimarySkillValue(PrimarySkill::ATTACK) + hero->getBasePrimarySkillValue(PrimarySkill::DEFENSE) + hero->getBasePrimarySkillValue(PrimarySkill::SPELL_POWER) + hero->getBasePrimarySkillValue(PrimarySkill::KNOWLEDGE);
|
||||||
}
|
}
|
||||||
|
|
||||||
void HeroManager::update()
|
void HeroManager::update()
|
||||||
@ -108,7 +108,7 @@ void HeroManager::update()
|
|||||||
for(auto & hero : myHeroes)
|
for(auto & hero : myHeroes)
|
||||||
{
|
{
|
||||||
scores[hero] = evaluateFightingStrength(hero);
|
scores[hero] = evaluateFightingStrength(hero);
|
||||||
knownFightingStrength[hero->id] = hero->getFightingStrength();
|
knownFightingStrength[hero->id] = hero->getHeroStrength();
|
||||||
}
|
}
|
||||||
|
|
||||||
auto scoreSort = [&](const CGHeroInstance * h1, const CGHeroInstance * h2) -> bool
|
auto scoreSort = [&](const CGHeroInstance * h1, const CGHeroInstance * h2) -> bool
|
||||||
@ -147,7 +147,10 @@ void HeroManager::update()
|
|||||||
|
|
||||||
HeroRole HeroManager::getHeroRole(const HeroPtr & hero) const
|
HeroRole HeroManager::getHeroRole(const HeroPtr & hero) const
|
||||||
{
|
{
|
||||||
return heroRoles.at(hero);
|
if (heroRoles.find(hero) != heroRoles.end())
|
||||||
|
return heroRoles.at(hero);
|
||||||
|
else
|
||||||
|
return HeroRole::SCOUT;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::map<HeroPtr, HeroRole> & HeroManager::getHeroRoles() const
|
const std::map<HeroPtr, HeroRole> & HeroManager::getHeroRoles() const
|
||||||
@ -188,13 +191,11 @@ float HeroManager::evaluateHero(const CGHeroInstance * hero) const
|
|||||||
return evaluateFightingStrength(hero);
|
return evaluateFightingStrength(hero);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool HeroManager::heroCapReached() const
|
bool HeroManager::heroCapReached(bool includeGarrisoned) const
|
||||||
{
|
{
|
||||||
const bool includeGarnisoned = true;
|
int heroCount = cb->getHeroCount(ai->playerID, includeGarrisoned);
|
||||||
int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned);
|
|
||||||
|
|
||||||
return heroCount >= ALLOWED_ROAMING_HEROES
|
return heroCount >= ai->settings->getMaxRoamingHeroes()
|
||||||
|| heroCount >= ai->settings->getMaxRoamingHeroes()
|
|
||||||
|| heroCount >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)
|
|| heroCount >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)
|
||||||
|| heroCount >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP);
|
|| heroCount >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP);
|
||||||
}
|
}
|
||||||
@ -204,7 +205,7 @@ float HeroManager::getFightingStrengthCached(const CGHeroInstance * hero) const
|
|||||||
auto cached = knownFightingStrength.find(hero->id);
|
auto cached = knownFightingStrength.find(hero->id);
|
||||||
|
|
||||||
//FIXME: fallback to hero->getFightingStrength() is VERY slow on higher difficulties (no object graph? map reveal?)
|
//FIXME: fallback to hero->getFightingStrength() is VERY slow on higher difficulties (no object graph? map reveal?)
|
||||||
return cached != knownFightingStrength.end() ? cached->second : hero->getFightingStrength();
|
return cached != knownFightingStrength.end() ? cached->second : hero->getHeroStrength();
|
||||||
}
|
}
|
||||||
|
|
||||||
float HeroManager::getMagicStrength(const CGHeroInstance * hero) const
|
float HeroManager::getMagicStrength(const CGHeroInstance * hero) const
|
||||||
@ -281,7 +282,7 @@ const CGHeroInstance * HeroManager::findHeroWithGrail() const
|
|||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
|
|
||||||
const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) const
|
const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit, const CGTownInstance* townToSpare) const
|
||||||
{
|
{
|
||||||
const CGHeroInstance * weakestHero = nullptr;
|
const CGHeroInstance * weakestHero = nullptr;
|
||||||
auto myHeroes = ai->cb->getHeroesInfo();
|
auto myHeroes = ai->cb->getHeroesInfo();
|
||||||
@ -292,12 +293,13 @@ const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) co
|
|||||||
|| existingHero->getArmyStrength() >armyLimit
|
|| existingHero->getArmyStrength() >armyLimit
|
||||||
|| getHeroRole(existingHero) == HeroRole::MAIN
|
|| getHeroRole(existingHero) == HeroRole::MAIN
|
||||||
|| existingHero->movementPointsRemaining()
|
|| existingHero->movementPointsRemaining()
|
||||||
|
|| (townToSpare != nullptr && existingHero->visitedTown == townToSpare)
|
||||||
|| existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1))
|
|| existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength())
|
if(!weakestHero || weakestHero->getHeroStrength() > existingHero->getHeroStrength())
|
||||||
{
|
{
|
||||||
weakestHero = existingHero;
|
weakestHero = existingHero;
|
||||||
}
|
}
|
||||||
|
@ -56,9 +56,9 @@ public:
|
|||||||
float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const;
|
float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const;
|
||||||
float evaluateHero(const CGHeroInstance * hero) const;
|
float evaluateHero(const CGHeroInstance * hero) const;
|
||||||
bool canRecruitHero(const CGTownInstance * t = nullptr) const;
|
bool canRecruitHero(const CGTownInstance * t = nullptr) const;
|
||||||
bool heroCapReached() const;
|
bool heroCapReached(bool includeGarrisoned = true) const;
|
||||||
const CGHeroInstance * findHeroWithGrail() const;
|
const CGHeroInstance * findHeroWithGrail() const;
|
||||||
const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const;
|
const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit, const CGTownInstance * townToSpare = nullptr) const;
|
||||||
float getMagicStrength(const CGHeroInstance * hero) const;
|
float getMagicStrength(const CGHeroInstance * hero) const;
|
||||||
float getFightingStrengthCached(const CGHeroInstance * hero) const;
|
float getFightingStrengthCached(const CGHeroInstance * hero) const;
|
||||||
|
|
||||||
|
@ -97,9 +97,10 @@ std::optional<const CGObjectInstance *> ObjectClusterizer::getBlocker(const AIPa
|
|||||||
{
|
{
|
||||||
auto guardPos = ai->cb->getGuardingCreaturePosition(node.coord);
|
auto guardPos = ai->cb->getGuardingCreaturePosition(node.coord);
|
||||||
|
|
||||||
blockers = ai->cb->getVisitableObjs(node.coord);
|
if (ai->cb->isVisible(node.coord))
|
||||||
|
blockers = ai->cb->getVisitableObjs(node.coord);
|
||||||
|
|
||||||
if(guardPos.valid())
|
if(guardPos.valid() && ai->cb->isVisible(guardPos))
|
||||||
{
|
{
|
||||||
auto guard = ai->cb->getTopObj(ai->cb->getGuardingCreaturePosition(node.coord));
|
auto guard = ai->cb->getTopObj(ai->cb->getGuardingCreaturePosition(node.coord));
|
||||||
|
|
||||||
@ -474,9 +475,11 @@ void ObjectClusterizer::clusterizeObject(
|
|||||||
|
|
||||||
heroesProcessed.insert(path.targetHero);
|
heroesProcessed.insert(path.targetHero);
|
||||||
|
|
||||||
float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)));
|
float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), PriorityEvaluator::PriorityTier::HUNTER_GATHER);
|
||||||
|
|
||||||
if(priority < MIN_PRIORITY)
|
if(ai->settings->isUseFuzzy() && priority < MIN_PRIORITY)
|
||||||
|
continue;
|
||||||
|
else if (priority <= 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
ClusterMap::accessor cluster;
|
ClusterMap::accessor cluster;
|
||||||
@ -495,9 +498,11 @@ void ObjectClusterizer::clusterizeObject(
|
|||||||
|
|
||||||
heroesProcessed.insert(path.targetHero);
|
heroesProcessed.insert(path.targetHero);
|
||||||
|
|
||||||
float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)));
|
float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), PriorityEvaluator::PriorityTier::HUNTER_GATHER);
|
||||||
|
|
||||||
if(priority < MIN_PRIORITY)
|
if (ai->settings->isUseFuzzy() && priority < MIN_PRIORITY)
|
||||||
|
continue;
|
||||||
|
else if (priority <= 0)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
bool interestingObject = path.turn() <= 2 || priority > 0.5f;
|
bool interestingObject = path.turn() <= 2 || priority > 0.5f;
|
||||||
|
@ -49,26 +49,49 @@ Goals::TGoalVec BuildingBehavior::decompose(const Nullkiller * ai) const
|
|||||||
auto & developmentInfos = ai->buildAnalyzer->getDevelopmentInfo();
|
auto & developmentInfos = ai->buildAnalyzer->getDevelopmentInfo();
|
||||||
auto isGoldPressureLow = !ai->buildAnalyzer->isGoldPressureHigh();
|
auto isGoldPressureLow = !ai->buildAnalyzer->isGoldPressureHigh();
|
||||||
|
|
||||||
|
ai->dangerHitMap->updateHitMap();
|
||||||
|
|
||||||
for(auto & developmentInfo : developmentInfos)
|
for(auto & developmentInfo : developmentInfos)
|
||||||
{
|
{
|
||||||
for(auto & buildingInfo : developmentInfo.toBuild)
|
bool emergencyDefense = false;
|
||||||
|
uint8_t closestThreat = std::numeric_limits<uint8_t>::max();
|
||||||
|
for (auto threat : ai->dangerHitMap->getTownThreats(developmentInfo.town))
|
||||||
{
|
{
|
||||||
if(isGoldPressureLow || buildingInfo.dailyIncome[EGameResID::GOLD] > 0)
|
closestThreat = std::min(closestThreat, threat.turn);
|
||||||
|
}
|
||||||
|
for (auto& buildingInfo : developmentInfo.toBuild)
|
||||||
|
{
|
||||||
|
if (closestThreat <= 1 && developmentInfo.town->fortLevel() < CGTownInstance::EFortLevel::CASTLE && !buildingInfo.notEnoughRes)
|
||||||
{
|
{
|
||||||
if(buildingInfo.notEnoughRes)
|
if (buildingInfo.id == BuildingID::CITADEL || buildingInfo.id == BuildingID::CASTLE)
|
||||||
{
|
{
|
||||||
if(ai->getLockedResources().canAfford(buildingInfo.buildCost))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
Composition composition;
|
|
||||||
|
|
||||||
composition.addNext(BuildThis(buildingInfo, developmentInfo));
|
|
||||||
composition.addNext(SaveResources(buildingInfo.buildCost));
|
|
||||||
|
|
||||||
tasks.push_back(sptr(composition));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo)));
|
tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo)));
|
||||||
|
emergencyDefense = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!emergencyDefense)
|
||||||
|
{
|
||||||
|
for (auto& buildingInfo : developmentInfo.toBuild)
|
||||||
|
{
|
||||||
|
if (isGoldPressureLow || buildingInfo.dailyIncome[EGameResID::GOLD] > 0)
|
||||||
|
{
|
||||||
|
if (buildingInfo.notEnoughRes)
|
||||||
|
{
|
||||||
|
if (ai->getLockedResources().canAfford(buildingInfo.buildCost))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
Composition composition;
|
||||||
|
|
||||||
|
composition.addNext(BuildThis(buildingInfo, developmentInfo));
|
||||||
|
composition.addNext(SaveResources(buildingInfo.buildCost));
|
||||||
|
tasks.push_back(sptr(composition));
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo)));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,9 +28,6 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const
|
|||||||
{
|
{
|
||||||
Goals::TGoalVec tasks;
|
Goals::TGoalVec tasks;
|
||||||
|
|
||||||
if(ai->cb->getDate(Date::DAY) == 1)
|
|
||||||
return tasks;
|
|
||||||
|
|
||||||
auto heroes = cb->getHeroesInfo();
|
auto heroes = cb->getHeroesInfo();
|
||||||
|
|
||||||
if(heroes.empty())
|
if(heroes.empty())
|
||||||
@ -38,19 +35,23 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const
|
|||||||
return tasks;
|
return tasks;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ai->dangerHitMap->updateHitMap();
|
||||||
|
|
||||||
for(auto town : cb->getTownsInfo())
|
for(auto town : cb->getTownsInfo())
|
||||||
{
|
{
|
||||||
|
uint8_t closestThreat = ai->dangerHitMap->getTileThreat(town->visitablePos()).fastestDanger.turn;
|
||||||
|
|
||||||
|
if (closestThreat >=2 && ai->buildAnalyzer->isGoldPressureHigh() && !town->hasBuilt(BuildingID::CITY_HALL) && cb->canBuildStructure(town, BuildingID::CITY_HALL) != EBuildingState::FORBIDDEN)
|
||||||
|
{
|
||||||
|
return tasks;
|
||||||
|
}
|
||||||
|
|
||||||
auto townArmyAvailableToBuy = ai->armyManager->getArmyAvailableToBuyAsCCreatureSet(
|
auto townArmyAvailableToBuy = ai->armyManager->getArmyAvailableToBuyAsCCreatureSet(
|
||||||
town,
|
town,
|
||||||
ai->getFreeResources());
|
ai->getFreeResources());
|
||||||
|
|
||||||
for(const CGHeroInstance * targetHero : heroes)
|
for(const CGHeroInstance * targetHero : heroes)
|
||||||
{
|
{
|
||||||
if(ai->buildAnalyzer->isGoldPressureHigh() && !town->hasBuilt(BuildingID::CITY_HALL))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(ai->heroManager->getHeroRole(targetHero) == HeroRole::MAIN)
|
if(ai->heroManager->getHeroRole(targetHero) == HeroRole::MAIN)
|
||||||
{
|
{
|
||||||
auto reinforcement = ai->armyManager->howManyReinforcementsCanGet(
|
auto reinforcement = ai->armyManager->howManyReinforcementsCanGet(
|
||||||
|
@ -68,14 +68,6 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(
|
|||||||
logAi->trace("Path found %s", path.toString());
|
logAi->trace("Path found %s", path.toString());
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if(nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
|
|
||||||
{
|
|
||||||
#if NKAI_TRACE_LEVEL >= 2
|
|
||||||
logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.getHeroStrength());
|
|
||||||
#endif
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(objToVisit && !force && !shouldVisit(nullkiller, path.targetHero, objToVisit))
|
if(objToVisit && !force && !shouldVisit(nullkiller, path.targetHero, objToVisit))
|
||||||
{
|
{
|
||||||
#if NKAI_TRACE_LEVEL >= 2
|
#if NKAI_TRACE_LEVEL >= 2
|
||||||
@ -87,6 +79,9 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(
|
|||||||
auto hero = path.targetHero;
|
auto hero = path.targetHero;
|
||||||
auto danger = path.getTotalDanger();
|
auto danger = path.getTotalDanger();
|
||||||
|
|
||||||
|
if (hero->getOwner() != nullkiller->playerID)
|
||||||
|
continue;
|
||||||
|
|
||||||
if(nullkiller->heroManager->getHeroRole(hero) == HeroRole::SCOUT
|
if(nullkiller->heroManager->getHeroRole(hero) == HeroRole::SCOUT
|
||||||
&& (path.getTotalDanger() == 0 || path.turn() > 0)
|
&& (path.getTotalDanger() == 0 || path.turn() > 0)
|
||||||
&& path.exchangeCount > 1)
|
&& path.exchangeCount > 1)
|
||||||
@ -119,7 +114,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto isSafe = isSafeToVisit(hero, path.heroArmy, danger);
|
auto isSafe = isSafeToVisit(hero, path.heroArmy, danger, nullkiller->settings->getSafeAttackRatio());
|
||||||
|
|
||||||
#if NKAI_TRACE_LEVEL >= 2
|
#if NKAI_TRACE_LEVEL >= 2
|
||||||
logAi->trace(
|
logAi->trace(
|
||||||
|
@ -41,6 +41,9 @@ Goals::TGoalVec DefenceBehavior::decompose(const Nullkiller * ai) const
|
|||||||
for(auto town : ai->cb->getTownsInfo())
|
for(auto town : ai->cb->getTownsInfo())
|
||||||
{
|
{
|
||||||
evaluateDefence(tasks, town, ai);
|
evaluateDefence(tasks, town, ai);
|
||||||
|
//Let's do only one defence-task per pass since otherwise it can try to hire the same hero twice
|
||||||
|
if (!tasks.empty())
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
return tasks;
|
return tasks;
|
||||||
@ -130,7 +133,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa
|
|||||||
|
|
||||||
tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5)));
|
tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5)));
|
||||||
|
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
else if(ai->heroManager->getHeroRole(town->garrisonHero.get()) == HeroRole::MAIN)
|
else if(ai->heroManager->getHeroRole(town->garrisonHero.get()) == HeroRole::MAIN)
|
||||||
{
|
{
|
||||||
@ -141,7 +144,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa
|
|||||||
{
|
{
|
||||||
tasks.push_back(Goals::sptr(Goals::DismissHero(heroToDismiss).setpriority(5)));
|
tasks.push_back(Goals::sptr(Goals::DismissHero(heroToDismiss).setpriority(5)));
|
||||||
|
|
||||||
return true;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -158,11 +161,10 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
|
|||||||
|
|
||||||
threats.push_back(threatNode.fastestDanger); // no guarantee that fastest danger will be there
|
threats.push_back(threatNode.fastestDanger); // no guarantee that fastest danger will be there
|
||||||
|
|
||||||
if(town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks, ai))
|
if (town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks, ai))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(!threatNode.fastestDanger.hero)
|
if(!threatNode.fastestDanger.hero)
|
||||||
{
|
{
|
||||||
logAi->trace("No threat found for town %s", town->getNameTranslated());
|
logAi->trace("No threat found for town %s", town->getNameTranslated());
|
||||||
@ -250,6 +252,16 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!path.targetHero->canBeMergedWith(*town))
|
||||||
|
{
|
||||||
|
#if NKAI_TRACE_LEVEL >= 1
|
||||||
|
logAi->trace("Can't merge armies of hero %s and town %s",
|
||||||
|
path.targetHero->getObjectName(),
|
||||||
|
town->getObjectName());
|
||||||
|
#endif
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if(path.targetHero == town->visitingHero.get() && path.exchangeCount == 1)
|
if(path.targetHero == town->visitingHero.get() && path.exchangeCount == 1)
|
||||||
{
|
{
|
||||||
#if NKAI_TRACE_LEVEL >= 1
|
#if NKAI_TRACE_LEVEL >= 1
|
||||||
@ -261,6 +273,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
|
|||||||
// dismiss creatures we are not able to pick to be able to hide in garrison
|
// dismiss creatures we are not able to pick to be able to hide in garrison
|
||||||
if(town->garrisonHero
|
if(town->garrisonHero
|
||||||
|| town->getUpperArmy()->stacksCount() == 0
|
|| town->getUpperArmy()->stacksCount() == 0
|
||||||
|
|| path.targetHero->canBeMergedWith(*town)
|
||||||
|| (town->getUpperArmy()->getArmyStrength() < 500 && town->fortLevel() >= CGTownInstance::CITADEL))
|
|| (town->getUpperArmy()->getArmyStrength() < 500 && town->fortLevel() >= CGTownInstance::CITADEL))
|
||||||
{
|
{
|
||||||
tasks.push_back(
|
tasks.push_back(
|
||||||
@ -292,7 +305,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(threat.turn == 0 || (path.turn() <= threat.turn && path.getHeroStrength() * SAFE_ATTACK_CONSTANT >= threat.danger))
|
if(threat.turn == 0 || (path.turn() <= threat.turn && path.getHeroStrength() * ai->settings->getSafeAttackRatio() >= threat.danger))
|
||||||
{
|
{
|
||||||
if(ai->arePathHeroesLocked(path))
|
if(ai->arePathHeroesLocked(path))
|
||||||
{
|
{
|
||||||
@ -343,23 +356,14 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
|
|||||||
}
|
}
|
||||||
else if(town->visitingHero && path.targetHero != town->visitingHero && !path.containsHero(town->visitingHero))
|
else if(town->visitingHero && path.targetHero != town->visitingHero && !path.containsHero(town->visitingHero))
|
||||||
{
|
{
|
||||||
if(town->garrisonHero)
|
if(town->garrisonHero && town->garrisonHero != path.targetHero)
|
||||||
{
|
{
|
||||||
if(ai->heroManager->getHeroRole(town->visitingHero.get()) == HeroRole::SCOUT
|
|
||||||
&& town->visitingHero->getArmyStrength() < path.heroArmy->getArmyStrength() / 20)
|
|
||||||
{
|
|
||||||
if(path.turn() == 0)
|
|
||||||
sequence.push_back(sptr(DismissHero(town->visitingHero.get())));
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
#if NKAI_TRACE_LEVEL >= 1
|
#if NKAI_TRACE_LEVEL >= 1
|
||||||
logAi->trace("Cancel moving %s to defend town %s as the town has garrison hero",
|
logAi->trace("Cancel moving %s to defend town %s as the town has garrison hero",
|
||||||
path.targetHero->getObjectName(),
|
path.targetHero->getObjectName(),
|
||||||
town->getObjectName());
|
town->getObjectName());
|
||||||
#endif
|
#endif
|
||||||
continue;
|
continue;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else if(path.turn() == 0)
|
else if(path.turn() == 0)
|
||||||
{
|
{
|
||||||
@ -405,6 +409,9 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
|
|||||||
|
|
||||||
void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & threat, const CGTownInstance * town, const Nullkiller * ai) const
|
void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & threat, const CGTownInstance * town, const Nullkiller * ai) const
|
||||||
{
|
{
|
||||||
|
if (threat.turn > 0 || town->garrisonHero || town->visitingHero)
|
||||||
|
return;
|
||||||
|
|
||||||
if(town->hasBuilt(BuildingID::TAVERN)
|
if(town->hasBuilt(BuildingID::TAVERN)
|
||||||
&& ai->cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST)
|
&& ai->cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST)
|
||||||
{
|
{
|
||||||
@ -451,7 +458,7 @@ void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitM
|
|||||||
}
|
}
|
||||||
else if(ai->heroManager->heroCapReached())
|
else if(ai->heroManager->heroCapReached())
|
||||||
{
|
{
|
||||||
heroToDismiss = ai->heroManager->findWeakHeroToDismiss(hero->getArmyStrength());
|
heroToDismiss = ai->heroManager->findWeakHeroToDismiss(hero->getArmyStrength(), town);
|
||||||
|
|
||||||
if(!heroToDismiss)
|
if(!heroToDismiss)
|
||||||
continue;
|
continue;
|
||||||
|
@ -33,48 +33,32 @@ Goals::TGoalVec ExplorationBehavior::decompose(const Nullkiller * ai) const
|
|||||||
{
|
{
|
||||||
Goals::TGoalVec tasks;
|
Goals::TGoalVec tasks;
|
||||||
|
|
||||||
for(auto obj : ai->memory->visitableObjs)
|
for (auto obj : ai->memory->visitableObjs)
|
||||||
{
|
{
|
||||||
if(!vstd::contains(ai->memory->alreadyVisited, obj))
|
switch (obj->ID.num)
|
||||||
{
|
{
|
||||||
switch(obj->ID.num)
|
|
||||||
{
|
|
||||||
case Obj::REDWOOD_OBSERVATORY:
|
case Obj::REDWOOD_OBSERVATORY:
|
||||||
case Obj::PILLAR_OF_FIRE:
|
case Obj::PILLAR_OF_FIRE:
|
||||||
tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 200)).addNext(CaptureObject(obj))));
|
{
|
||||||
|
auto rObj = dynamic_cast<const CRewardableObject*>(obj);
|
||||||
|
if (!rObj->wasScouted(ai->playerID))
|
||||||
|
tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 200)).addNext(CaptureObject(obj))));
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case Obj::MONOLITH_ONE_WAY_ENTRANCE:
|
case Obj::MONOLITH_ONE_WAY_ENTRANCE:
|
||||||
case Obj::MONOLITH_TWO_WAY:
|
case Obj::MONOLITH_TWO_WAY:
|
||||||
case Obj::SUBTERRANEAN_GATE:
|
case Obj::SUBTERRANEAN_GATE:
|
||||||
case Obj::WHIRLPOOL:
|
case Obj::WHIRLPOOL:
|
||||||
auto tObj = dynamic_cast<const CGTeleport *>(obj);
|
|
||||||
if(TeleportChannel::IMPASSABLE != ai->memory->knownTeleportChannels[tObj->channel]->passability)
|
|
||||||
{
|
|
||||||
tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj))));
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
switch(obj->ID.num)
|
|
||||||
{
|
{
|
||||||
case Obj::MONOLITH_TWO_WAY:
|
auto tObj = dynamic_cast<const CGTeleport*>(obj);
|
||||||
case Obj::SUBTERRANEAN_GATE:
|
for (auto exit : cb->getTeleportChannelExits(tObj->channel))
|
||||||
case Obj::WHIRLPOOL:
|
|
||||||
auto tObj = dynamic_cast<const CGTeleport *>(obj);
|
|
||||||
if(TeleportChannel::IMPASSABLE == ai->memory->knownTeleportChannels[tObj->channel]->passability)
|
|
||||||
break;
|
|
||||||
for(auto exit : ai->memory->knownTeleportChannels[tObj->channel]->exits)
|
|
||||||
{
|
{
|
||||||
if(!cb->getObj(exit))
|
if (exit != tObj->id)
|
||||||
{
|
{
|
||||||
// Always attempt to visit two-way teleports if one of channel exits is not visible
|
if (!cb->isVisible(cb->getObjInstance(exit)))
|
||||||
tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj))));
|
tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj))));
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -81,6 +81,9 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con
|
|||||||
logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength());
|
logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength());
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
if (path.targetHero->getOwner() != ai->playerID)
|
||||||
|
continue;
|
||||||
|
|
||||||
if(path.containsHero(hero))
|
if(path.containsHero(hero))
|
||||||
{
|
{
|
||||||
#if NKAI_TRACE_LEVEL >= 2
|
#if NKAI_TRACE_LEVEL >= 2
|
||||||
@ -89,14 +92,6 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(path.turn() > 0 && ai->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
|
|
||||||
{
|
|
||||||
#if NKAI_TRACE_LEVEL >= 2
|
|
||||||
logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength());
|
|
||||||
#endif
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(ai->arePathHeroesLocked(path))
|
if(ai->arePathHeroesLocked(path))
|
||||||
{
|
{
|
||||||
#if NKAI_TRACE_LEVEL >= 2
|
#if NKAI_TRACE_LEVEL >= 2
|
||||||
@ -150,7 +145,7 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con
|
|||||||
}
|
}
|
||||||
|
|
||||||
auto danger = path.getTotalDanger();
|
auto danger = path.getTotalDanger();
|
||||||
auto isSafe = isSafeToVisit(hero, path.heroArmy, danger);
|
auto isSafe = isSafeToVisit(hero, path.heroArmy, danger, ai->settings->getSafeAttackRatio());
|
||||||
|
|
||||||
#if NKAI_TRACE_LEVEL >= 2
|
#if NKAI_TRACE_LEVEL >= 2
|
||||||
logAi->trace(
|
logAi->trace(
|
||||||
@ -292,17 +287,6 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto heroRole = ai->heroManager->getHeroRole(path.targetHero);
|
|
||||||
|
|
||||||
if(heroRole == HeroRole::SCOUT
|
|
||||||
&& ai->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
|
|
||||||
{
|
|
||||||
#if NKAI_TRACE_LEVEL >= 2
|
|
||||||
logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength());
|
|
||||||
#endif
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
auto upgrade = ai->armyManager->calculateCreaturesUpgrade(path.heroArmy, upgrader, availableResources);
|
auto upgrade = ai->armyManager->calculateCreaturesUpgrade(path.heroArmy, upgrader, availableResources);
|
||||||
|
|
||||||
if(!upgrader->garrisonHero
|
if(!upgrader->garrisonHero
|
||||||
@ -320,14 +304,6 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
|
|||||||
|
|
||||||
armyToGetOrBuy.upgradeValue -= path.heroArmy->getArmyStrength();
|
armyToGetOrBuy.upgradeValue -= path.heroArmy->getArmyStrength();
|
||||||
|
|
||||||
armyToGetOrBuy.addArmyToBuy(
|
|
||||||
ai->armyManager->toSlotInfo(
|
|
||||||
ai->armyManager->getArmyAvailableToBuy(
|
|
||||||
path.heroArmy,
|
|
||||||
upgrader,
|
|
||||||
ai->getFreeResources(),
|
|
||||||
path.turn())));
|
|
||||||
|
|
||||||
upgrade.upgradeValue += armyToGetOrBuy.upgradeValue;
|
upgrade.upgradeValue += armyToGetOrBuy.upgradeValue;
|
||||||
upgrade.upgradeCost += armyToGetOrBuy.upgradeCost;
|
upgrade.upgradeCost += armyToGetOrBuy.upgradeCost;
|
||||||
vstd::concatenate(upgrade.resultingArmy, armyToGetOrBuy.resultingArmy);
|
vstd::concatenate(upgrade.resultingArmy, armyToGetOrBuy.resultingArmy);
|
||||||
@ -339,8 +315,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
|
|||||||
{
|
{
|
||||||
for(auto hero : cb->getAvailableHeroes(upgrader))
|
for(auto hero : cb->getAvailableHeroes(upgrader))
|
||||||
{
|
{
|
||||||
auto scoutReinforcement = ai->armyManager->howManyReinforcementsCanBuy(hero, upgrader)
|
auto scoutReinforcement = ai->armyManager->howManyReinforcementsCanGet(hero, upgrader);
|
||||||
+ ai->armyManager->howManyReinforcementsCanGet(hero, upgrader);
|
|
||||||
|
|
||||||
if(scoutReinforcement >= armyToGetOrBuy.upgradeValue
|
if(scoutReinforcement >= armyToGetOrBuy.upgradeValue
|
||||||
&& ai->getFreeGold() >20000
|
&& ai->getFreeGold() >20000
|
||||||
@ -366,7 +341,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
|
|||||||
|
|
||||||
auto danger = path.getTotalDanger();
|
auto danger = path.getTotalDanger();
|
||||||
|
|
||||||
auto isSafe = isSafeToVisit(path.targetHero, path.heroArmy, danger);
|
auto isSafe = isSafeToVisit(path.targetHero, path.heroArmy, danger, ai->settings->getSafeAttackRatio());
|
||||||
|
|
||||||
#if NKAI_TRACE_LEVEL >= 2
|
#if NKAI_TRACE_LEVEL >= 2
|
||||||
logAi->trace(
|
logAi->trace(
|
||||||
|
@ -31,9 +31,11 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
|
|||||||
|
|
||||||
auto ourHeroes = ai->heroManager->getHeroRoles();
|
auto ourHeroes = ai->heroManager->getHeroRoles();
|
||||||
auto minScoreToHireMain = std::numeric_limits<float>::max();
|
auto minScoreToHireMain = std::numeric_limits<float>::max();
|
||||||
|
int currentArmyValue = 0;
|
||||||
|
|
||||||
for(auto hero : ourHeroes)
|
for(auto hero : ourHeroes)
|
||||||
{
|
{
|
||||||
|
currentArmyValue += hero.first->getArmyCost();
|
||||||
if(hero.second != HeroRole::MAIN)
|
if(hero.second != HeroRole::MAIN)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
@ -45,51 +47,88 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
|
|||||||
minScoreToHireMain = newScore;
|
minScoreToHireMain = newScore;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// If we don't have any heros we might want to lower our expectations.
|
||||||
|
if (ourHeroes.empty())
|
||||||
|
minScoreToHireMain = 0;
|
||||||
|
|
||||||
|
const CGHeroInstance* bestHeroToHire = nullptr;
|
||||||
|
const CGTownInstance* bestTownToHireFrom = nullptr;
|
||||||
|
float bestScore = 0;
|
||||||
|
bool haveCapitol = false;
|
||||||
|
|
||||||
|
ai->dangerHitMap->updateHitMap();
|
||||||
|
int treasureSourcesCount = 0;
|
||||||
|
|
||||||
for(auto town : towns)
|
for(auto town : towns)
|
||||||
{
|
{
|
||||||
|
uint8_t closestThreat = UINT8_MAX;
|
||||||
|
for (auto threat : ai->dangerHitMap->getTownThreats(town))
|
||||||
|
{
|
||||||
|
closestThreat = std::min(closestThreat, threat.turn);
|
||||||
|
}
|
||||||
|
//Don't hire a hero where there already is one present
|
||||||
|
if (town->visitingHero && town->garrisonHero)
|
||||||
|
continue;
|
||||||
|
float visitability = 0;
|
||||||
|
for (auto checkHero : ourHeroes)
|
||||||
|
{
|
||||||
|
if (ai->dangerHitMap->getClosestTown(checkHero.first.get()->visitablePos()) == town)
|
||||||
|
visitability++;
|
||||||
|
}
|
||||||
if(ai->heroManager->canRecruitHero(town))
|
if(ai->heroManager->canRecruitHero(town))
|
||||||
{
|
{
|
||||||
auto availableHeroes = ai->cb->getAvailableHeroes(town);
|
auto availableHeroes = ai->cb->getAvailableHeroes(town);
|
||||||
|
|
||||||
for(auto hero : availableHeroes)
|
for (auto obj : ai->objectClusterizer->getNearbyObjects())
|
||||||
{
|
{
|
||||||
auto score = ai->heroManager->evaluateHero(hero);
|
if ((obj->ID == Obj::RESOURCE)
|
||||||
|
|
||||||
if(score > minScoreToHireMain)
|
|
||||||
{
|
|
||||||
tasks.push_back(Goals::sptr(Goals::RecruitHero(town, hero).setpriority(200)));
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
int treasureSourcesCount = 0;
|
|
||||||
|
|
||||||
for(auto obj : ai->objectClusterizer->getNearbyObjects())
|
|
||||||
{
|
|
||||||
if((obj->ID == Obj::RESOURCE)
|
|
||||||
|| obj->ID == Obj::TREASURE_CHEST
|
|| obj->ID == Obj::TREASURE_CHEST
|
||||||
|| obj->ID == Obj::CAMPFIRE
|
|| obj->ID == Obj::CAMPFIRE
|
||||||
|| isWeeklyRevisitable(ai, obj)
|
|| isWeeklyRevisitable(ai, obj)
|
||||||
|| obj->ID ==Obj::ARTIFACT)
|
|| obj->ID == Obj::ARTIFACT)
|
||||||
{
|
{
|
||||||
auto tile = obj->visitablePos();
|
auto tile = obj->visitablePos();
|
||||||
auto closestTown = ai->dangerHitMap->getClosestTown(tile);
|
auto closestTown = ai->dangerHitMap->getClosestTown(tile);
|
||||||
|
|
||||||
if(town == closestTown)
|
if (town == closestTown)
|
||||||
treasureSourcesCount++;
|
treasureSourcesCount++;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if(treasureSourcesCount < 5 && (town->garrisonHero || town->getUpperArmy()->getArmyStrength() < 10000))
|
for(auto hero : availableHeroes)
|
||||||
continue;
|
|
||||||
|
|
||||||
if(ai->cb->getHeroesInfo().size() < ai->cb->getTownsInfo().size() + 1
|
|
||||||
|| (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh()))
|
|
||||||
{
|
{
|
||||||
tasks.push_back(Goals::sptr(Goals::RecruitHero(town).setpriority(3)));
|
auto score = ai->heroManager->evaluateHero(hero);
|
||||||
|
if(score > minScoreToHireMain)
|
||||||
|
{
|
||||||
|
score *= score / minScoreToHireMain;
|
||||||
|
}
|
||||||
|
score *= (hero->getArmyCost() + currentArmyValue);
|
||||||
|
if (hero->getFactionID() == town->getFactionID())
|
||||||
|
score *= 1.5;
|
||||||
|
if (vstd::isAlmostZero(visitability))
|
||||||
|
score *= 30 * town->getTownLevel();
|
||||||
|
else
|
||||||
|
score *= town->getTownLevel() / visitability;
|
||||||
|
if (score > bestScore)
|
||||||
|
{
|
||||||
|
bestScore = score;
|
||||||
|
bestHeroToHire = hero;
|
||||||
|
bestTownToHireFrom = town;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (town->hasCapitol())
|
||||||
|
haveCapitol = true;
|
||||||
|
}
|
||||||
|
if (bestHeroToHire && bestTownToHireFrom)
|
||||||
|
{
|
||||||
|
if (ai->cb->getHeroesInfo().size() == 0
|
||||||
|
|| treasureSourcesCount > ai->cb->getHeroesInfo().size() * 5
|
||||||
|
|| (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh() && haveCapitol)
|
||||||
|
|| (ai->getFreeResources()[EGameResID::GOLD] > 30000 && !ai->buildAnalyzer->isGoldPressureHigh()))
|
||||||
|
{
|
||||||
|
tasks.push_back(Goals::sptr(Goals::RecruitHero(bestTownToHireFrom, bestHeroToHire).setpriority((float)3 / (ourHeroes.size() + 1))));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return tasks;
|
return tasks;
|
||||||
|
@ -39,9 +39,6 @@ Goals::TGoalVec StayAtTownBehavior::decompose(const Nullkiller * ai) const
|
|||||||
|
|
||||||
for(auto town : towns)
|
for(auto town : towns)
|
||||||
{
|
{
|
||||||
if(!town->hasBuilt(BuildingID::MAGES_GUILD_1))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
ai->pathfinder->calculatePathInfo(paths, town->visitablePos());
|
ai->pathfinder->calculatePathInfo(paths, town->visitablePos());
|
||||||
|
|
||||||
for(auto & path : paths)
|
for(auto & path : paths)
|
||||||
@ -49,14 +46,8 @@ Goals::TGoalVec StayAtTownBehavior::decompose(const Nullkiller * ai) const
|
|||||||
if(town->visitingHero && town->visitingHero.get() != path.targetHero)
|
if(town->visitingHero && town->visitingHero.get() != path.targetHero)
|
||||||
continue;
|
continue;
|
||||||
|
|
||||||
if(!path.targetHero->hasSpellbook() || path.targetHero->mana >= 0.75f * path.targetHero->manaLimit())
|
if(!path.getFirstBlockedAction() && path.exchangeCount <= 1)
|
||||||
continue;
|
|
||||||
|
|
||||||
if(path.turn() == 0 && !path.getFirstBlockedAction() && path.exchangeCount <= 1)
|
|
||||||
{
|
{
|
||||||
if(path.targetHero->mana == path.targetHero->manaLimit())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
Composition stayAtTown;
|
Composition stayAtTown;
|
||||||
|
|
||||||
stayAtTown.addNextSequence({
|
stayAtTown.addNextSequence({
|
||||||
|
@ -17,8 +17,7 @@
|
|||||||
namespace NKAI
|
namespace NKAI
|
||||||
{
|
{
|
||||||
|
|
||||||
#define MIN_AI_STRENGTH (0.5f) //lower when combat AI gets smarter
|
constexpr float MIN_AI_STRENGTH = 0.5f; //lower when combat AI gets smarter
|
||||||
#define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us
|
|
||||||
|
|
||||||
engineBase::engineBase()
|
engineBase::engineBase()
|
||||||
{
|
{
|
||||||
|
@ -52,6 +52,15 @@ ui64 FuzzyHelper::evaluateDanger(const int3 & tile, const CGHeroInstance * visit
|
|||||||
{
|
{
|
||||||
objectDanger += evaluateDanger(hero->visitedTown.get());
|
objectDanger += evaluateDanger(hero->visitedTown.get());
|
||||||
}
|
}
|
||||||
|
objectDanger *= ai->heroManager->getFightingStrengthCached(hero);
|
||||||
|
}
|
||||||
|
if (objWithID<Obj::TOWN>(dangerousObject))
|
||||||
|
{
|
||||||
|
auto town = dynamic_cast<const CGTownInstance*>(dangerousObject);
|
||||||
|
auto hero = town->garrisonHero;
|
||||||
|
|
||||||
|
if (hero)
|
||||||
|
objectDanger *= ai->heroManager->getFightingStrengthCached(hero);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(objectDanger)
|
if(objectDanger)
|
||||||
@ -117,10 +126,10 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj)
|
|||||||
{
|
{
|
||||||
auto fortLevel = town->fortLevel();
|
auto fortLevel = town->fortLevel();
|
||||||
|
|
||||||
if(fortLevel == CGTownInstance::EFortLevel::CASTLE)
|
if (fortLevel == CGTownInstance::EFortLevel::CASTLE)
|
||||||
danger += 10000;
|
danger = std::max(danger * 2, danger + 10000);
|
||||||
else if(fortLevel == CGTownInstance::EFortLevel::CITADEL)
|
else if(fortLevel == CGTownInstance::EFortLevel::CITADEL)
|
||||||
danger += 4000;
|
danger = std::max(ui64(danger * 1.4), danger + 4000);
|
||||||
}
|
}
|
||||||
|
|
||||||
return danger;
|
return danger;
|
||||||
|
@ -34,13 +34,12 @@ using namespace Goals;
|
|||||||
std::unique_ptr<ObjectGraph> Nullkiller::baseGraph;
|
std::unique_ptr<ObjectGraph> Nullkiller::baseGraph;
|
||||||
|
|
||||||
Nullkiller::Nullkiller()
|
Nullkiller::Nullkiller()
|
||||||
:activeHero(nullptr), scanDepth(ScanDepth::MAIN_FULL), useHeroChain(true)
|
: activeHero(nullptr)
|
||||||
|
, scanDepth(ScanDepth::MAIN_FULL)
|
||||||
|
, useHeroChain(true)
|
||||||
|
, memory(std::make_unique<AIMemory>())
|
||||||
{
|
{
|
||||||
memory = std::make_unique<AIMemory>();
|
|
||||||
settings = std::make_unique<Settings>();
|
|
||||||
|
|
||||||
useObjectGraph = settings->isObjectGraphAllowed();
|
|
||||||
openMap = settings->isOpenMap() || useObjectGraph;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool canUseOpenMap(std::shared_ptr<CCallback> cb, PlayerColor playerID)
|
bool canUseOpenMap(std::shared_ptr<CCallback> cb, PlayerColor playerID)
|
||||||
@ -62,17 +61,23 @@ bool canUseOpenMap(std::shared_ptr<CCallback> cb, PlayerColor playerID)
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return cb->getStartInfo()->difficulty >= 3;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void Nullkiller::init(std::shared_ptr<CCallback> cb, AIGateway * gateway)
|
void Nullkiller::init(std::shared_ptr<CCallback> cb, AIGateway * gateway)
|
||||||
{
|
{
|
||||||
this->cb = cb;
|
this->cb = cb;
|
||||||
this->gateway = gateway;
|
this->gateway = gateway;
|
||||||
|
this->playerID = gateway->playerID;
|
||||||
|
|
||||||
playerID = gateway->playerID;
|
settings = std::make_unique<Settings>(cb->getStartInfo()->difficulty);
|
||||||
|
|
||||||
if(openMap && !canUseOpenMap(cb, playerID))
|
if(canUseOpenMap(cb, playerID))
|
||||||
|
{
|
||||||
|
useObjectGraph = settings->isObjectGraphAllowed();
|
||||||
|
openMap = settings->isOpenMap() || useObjectGraph;
|
||||||
|
}
|
||||||
|
else
|
||||||
{
|
{
|
||||||
useObjectGraph = false;
|
useObjectGraph = false;
|
||||||
openMap = false;
|
openMap = false;
|
||||||
@ -122,11 +127,14 @@ void TaskPlan::merge(TSubgoal task)
|
|||||||
{
|
{
|
||||||
TGoalVec blockers;
|
TGoalVec blockers;
|
||||||
|
|
||||||
|
if (task->asTask()->priority <= 0)
|
||||||
|
return;
|
||||||
|
|
||||||
for(auto & item : tasks)
|
for(auto & item : tasks)
|
||||||
{
|
{
|
||||||
for(auto objid : item.affectedObjects)
|
for(auto objid : item.affectedObjects)
|
||||||
{
|
{
|
||||||
if(task == item.task || task->asTask()->isObjectAffected(objid))
|
if(task == item.task || task->asTask()->isObjectAffected(objid) || (task->asTask()->getHero() != nullptr && task->asTask()->getHero() == item.task->asTask()->getHero()))
|
||||||
{
|
{
|
||||||
if(item.task->asTask()->priority >= task->asTask()->priority)
|
if(item.task->asTask()->priority >= task->asTask()->priority)
|
||||||
return;
|
return;
|
||||||
@ -166,20 +174,19 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TGoalVec & tasks) const
|
|||||||
return taskptr(*bestTask);
|
return taskptr(*bestTask);
|
||||||
}
|
}
|
||||||
|
|
||||||
Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks) const
|
Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks, int priorityTier) const
|
||||||
{
|
{
|
||||||
TaskPlan taskPlan;
|
TaskPlan taskPlan;
|
||||||
|
|
||||||
tbb::parallel_for(tbb::blocked_range<size_t>(0, tasks.size()), [this, &tasks](const tbb::blocked_range<size_t> & r)
|
tbb::parallel_for(tbb::blocked_range<size_t>(0, tasks.size()), [this, &tasks, priorityTier](const tbb::blocked_range<size_t> & r)
|
||||||
{
|
{
|
||||||
auto evaluator = this->priorityEvaluators->acquire();
|
auto evaluator = this->priorityEvaluators->acquire();
|
||||||
|
|
||||||
for(size_t i = r.begin(); i != r.end(); i++)
|
for(size_t i = r.begin(); i != r.end(); i++)
|
||||||
{
|
{
|
||||||
auto task = tasks[i];
|
auto task = tasks[i];
|
||||||
|
if (task->asTask()->priority <= 0 || priorityTier != PriorityEvaluator::PriorityTier::BUILDINGS)
|
||||||
if(task->asTask()->priority <= 0)
|
task->asTask()->priority = evaluator->evaluate(task, priorityTier);
|
||||||
task->asTask()->priority = evaluator->evaluate(task);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -326,7 +333,7 @@ bool Nullkiller::arePathHeroesLocked(const AIPath & path) const
|
|||||||
if(lockReason != HeroLockedReason::NOT_LOCKED)
|
if(lockReason != HeroLockedReason::NOT_LOCKED)
|
||||||
{
|
{
|
||||||
#if NKAI_TRACE_LEVEL >= 1
|
#if NKAI_TRACE_LEVEL >= 1
|
||||||
logAi->trace("Hero %s is locked by STARTUP. Discarding %s", path.targetHero->getObjectName(), path.toString());
|
logAi->trace("Hero %s is locked by %d. Discarding %s", path.targetHero->getObjectName(), (int)lockReason, path.toString());
|
||||||
#endif
|
#endif
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@ -347,12 +354,24 @@ void Nullkiller::makeTurn()
|
|||||||
boost::lock_guard<boost::mutex> sharedStorageLock(AISharedStorage::locker);
|
boost::lock_guard<boost::mutex> sharedStorageLock(AISharedStorage::locker);
|
||||||
|
|
||||||
const int MAX_DEPTH = 10;
|
const int MAX_DEPTH = 10;
|
||||||
const float FAST_TASK_MINIMAL_PRIORITY = 0.7f;
|
|
||||||
|
|
||||||
resetAiState();
|
resetAiState();
|
||||||
|
|
||||||
Goals::TGoalVec bestTasks;
|
Goals::TGoalVec bestTasks;
|
||||||
|
|
||||||
|
#if NKAI_TRACE_LEVEL >= 1
|
||||||
|
float totalHeroStrength = 0;
|
||||||
|
int totalTownLevel = 0;
|
||||||
|
for (auto heroInfo : cb->getHeroesInfo())
|
||||||
|
{
|
||||||
|
totalHeroStrength += heroInfo->getTotalStrength();
|
||||||
|
}
|
||||||
|
for (auto townInfo : cb->getTownsInfo())
|
||||||
|
{
|
||||||
|
totalTownLevel += townInfo->getTownLevel();
|
||||||
|
}
|
||||||
|
logAi->info("Beginning: Strength: %f Townlevel: %d Resources: %s", totalHeroStrength, totalTownLevel, cb->getResourceAmount().toString());
|
||||||
|
#endif
|
||||||
for(int i = 1; i <= settings->getMaxPass() && cb->getPlayerStatus(playerID) == EPlayerStatus::INGAME; i++)
|
for(int i = 1; i <= settings->getMaxPass() && cb->getPlayerStatus(playerID) == EPlayerStatus::INGAME; i++)
|
||||||
{
|
{
|
||||||
auto start = std::chrono::high_resolution_clock::now();
|
auto start = std::chrono::high_resolution_clock::now();
|
||||||
@ -360,17 +379,21 @@ void Nullkiller::makeTurn()
|
|||||||
|
|
||||||
Goals::TTask bestTask = taskptr(Goals::Invalid());
|
Goals::TTask bestTask = taskptr(Goals::Invalid());
|
||||||
|
|
||||||
for(;i <= settings->getMaxPass(); i++)
|
while(true)
|
||||||
{
|
{
|
||||||
bestTasks.clear();
|
bestTasks.clear();
|
||||||
|
|
||||||
|
decompose(bestTasks, sptr(RecruitHeroBehavior()), 1);
|
||||||
decompose(bestTasks, sptr(BuyArmyBehavior()), 1);
|
decompose(bestTasks, sptr(BuyArmyBehavior()), 1);
|
||||||
decompose(bestTasks, sptr(BuildingBehavior()), 1);
|
decompose(bestTasks, sptr(BuildingBehavior()), 1);
|
||||||
|
|
||||||
bestTask = choseBestTask(bestTasks);
|
bestTask = choseBestTask(bestTasks);
|
||||||
|
|
||||||
if(bestTask->priority >= FAST_TASK_MINIMAL_PRIORITY)
|
if(bestTask->priority > 0)
|
||||||
{
|
{
|
||||||
|
#if NKAI_TRACE_LEVEL >= 1
|
||||||
|
logAi->info("Pass %d: Performing prio 0 task %s with prio: %d", i, bestTask->toString(), bestTask->priority);
|
||||||
|
#endif
|
||||||
if(!executeTask(bestTask))
|
if(!executeTask(bestTask))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
@ -382,7 +405,6 @@ void Nullkiller::makeTurn()
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
decompose(bestTasks, sptr(RecruitHeroBehavior()), 1);
|
|
||||||
decompose(bestTasks, sptr(CaptureObjectsBehavior()), 1);
|
decompose(bestTasks, sptr(CaptureObjectsBehavior()), 1);
|
||||||
decompose(bestTasks, sptr(ClusterBehavior()), MAX_DEPTH);
|
decompose(bestTasks, sptr(ClusterBehavior()), MAX_DEPTH);
|
||||||
decompose(bestTasks, sptr(DefenceBehavior()), MAX_DEPTH);
|
decompose(bestTasks, sptr(DefenceBehavior()), MAX_DEPTH);
|
||||||
@ -392,12 +414,24 @@ void Nullkiller::makeTurn()
|
|||||||
if(!isOpenMap())
|
if(!isOpenMap())
|
||||||
decompose(bestTasks, sptr(ExplorationBehavior()), MAX_DEPTH);
|
decompose(bestTasks, sptr(ExplorationBehavior()), MAX_DEPTH);
|
||||||
|
|
||||||
if(cb->getDate(Date::DAY) == 1 || heroManager->getHeroRoles().empty())
|
TTaskVec selectedTasks;
|
||||||
|
#if NKAI_TRACE_LEVEL >= 1
|
||||||
|
int prioOfTask = 0;
|
||||||
|
#endif
|
||||||
|
for (int prio = PriorityEvaluator::PriorityTier::INSTAKILL; prio <= PriorityEvaluator::PriorityTier::DEFEND; ++prio)
|
||||||
{
|
{
|
||||||
decompose(bestTasks, sptr(StartupBehavior()), 1);
|
#if NKAI_TRACE_LEVEL >= 1
|
||||||
|
prioOfTask = prio;
|
||||||
|
#endif
|
||||||
|
selectedTasks = buildPlan(bestTasks, prio);
|
||||||
|
if (!selectedTasks.empty() || settings->isUseFuzzy())
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
auto selectedTasks = buildPlan(bestTasks);
|
std::sort(selectedTasks.begin(), selectedTasks.end(), [](const TTask& a, const TTask& b)
|
||||||
|
{
|
||||||
|
return a->priority > b->priority;
|
||||||
|
});
|
||||||
|
|
||||||
logAi->debug("Decision madel in %ld", timeElapsed(start));
|
logAi->debug("Decision madel in %ld", timeElapsed(start));
|
||||||
|
|
||||||
@ -438,7 +472,7 @@ void Nullkiller::makeTurn()
|
|||||||
bestTask->priority);
|
bestTask->priority);
|
||||||
}
|
}
|
||||||
|
|
||||||
if(bestTask->priority < MIN_PRIORITY)
|
if((settings->isUseFuzzy() && bestTask->priority < MIN_PRIORITY) || (!settings->isUseFuzzy() && bestTask->priority <= 0))
|
||||||
{
|
{
|
||||||
auto heroes = cb->getHeroesInfo();
|
auto heroes = cb->getHeroesInfo();
|
||||||
auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool
|
auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool
|
||||||
@ -463,7 +497,9 @@ void Nullkiller::makeTurn()
|
|||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
#if NKAI_TRACE_LEVEL >= 1
|
||||||
|
logAi->info("Pass %d: Performing prio %d task %s with prio: %d", i, prioOfTask, bestTask->toString(), bestTask->priority);
|
||||||
|
#endif
|
||||||
if(!executeTask(bestTask))
|
if(!executeTask(bestTask))
|
||||||
{
|
{
|
||||||
if(hasAnySuccess)
|
if(hasAnySuccess)
|
||||||
@ -471,13 +507,27 @@ void Nullkiller::makeTurn()
|
|||||||
else
|
else
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
hasAnySuccess = true;
|
hasAnySuccess = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasAnySuccess |= handleTrading();
|
||||||
|
|
||||||
if(!hasAnySuccess)
|
if(!hasAnySuccess)
|
||||||
{
|
{
|
||||||
logAi->trace("Nothing was done this turn. Ending turn.");
|
logAi->trace("Nothing was done this turn. Ending turn.");
|
||||||
|
#if NKAI_TRACE_LEVEL >= 1
|
||||||
|
totalHeroStrength = 0;
|
||||||
|
totalTownLevel = 0;
|
||||||
|
for (auto heroInfo : cb->getHeroesInfo())
|
||||||
|
{
|
||||||
|
totalHeroStrength += heroInfo->getTotalStrength();
|
||||||
|
}
|
||||||
|
for (auto townInfo : cb->getTownsInfo())
|
||||||
|
{
|
||||||
|
totalTownLevel += townInfo->getTownLevel();
|
||||||
|
}
|
||||||
|
logAi->info("End: Strength: %f Townlevel: %d Resources: %s", totalHeroStrength, totalTownLevel, cb->getResourceAmount().toString());
|
||||||
|
#endif
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -554,4 +604,102 @@ void Nullkiller::lockResources(const TResources & res)
|
|||||||
lockedResources += res;
|
lockedResources += res;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool Nullkiller::handleTrading()
|
||||||
|
{
|
||||||
|
bool haveTraded = false;
|
||||||
|
bool shouldTryToTrade = true;
|
||||||
|
int marketId = -1;
|
||||||
|
for (auto town : cb->getTownsInfo())
|
||||||
|
{
|
||||||
|
if (town->hasBuiltSomeTradeBuilding())
|
||||||
|
{
|
||||||
|
marketId = town->id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (marketId == -1)
|
||||||
|
return false;
|
||||||
|
if (const CGObjectInstance* obj = cb->getObj(ObjectInstanceID(marketId), false))
|
||||||
|
{
|
||||||
|
if (const auto* m = dynamic_cast<const IMarket*>(obj))
|
||||||
|
{
|
||||||
|
while (shouldTryToTrade)
|
||||||
|
{
|
||||||
|
shouldTryToTrade = false;
|
||||||
|
buildAnalyzer->update();
|
||||||
|
TResources required = buildAnalyzer->getTotalResourcesRequired();
|
||||||
|
TResources income = buildAnalyzer->getDailyIncome();
|
||||||
|
TResources available = cb->getResourceAmount();
|
||||||
|
#if NKAI_TRACE_LEVEL >= 2
|
||||||
|
logAi->debug("Available %s", available.toString());
|
||||||
|
logAi->debug("Required %s", required.toString());
|
||||||
|
#endif
|
||||||
|
int mostWanted = -1;
|
||||||
|
int mostExpendable = -1;
|
||||||
|
float minRatio = std::numeric_limits<float>::max();
|
||||||
|
float maxRatio = std::numeric_limits<float>::min();
|
||||||
|
|
||||||
|
for (int i = 0; i < required.size(); ++i)
|
||||||
|
{
|
||||||
|
if (required[i] <= 0)
|
||||||
|
continue;
|
||||||
|
float ratio = static_cast<float>(available[i]) / required[i];
|
||||||
|
|
||||||
|
if (ratio < minRatio) {
|
||||||
|
minRatio = ratio;
|
||||||
|
mostWanted = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < required.size(); ++i)
|
||||||
|
{
|
||||||
|
float ratio = available[i];
|
||||||
|
if (required[i] > 0)
|
||||||
|
ratio = static_cast<float>(available[i]) / required[i];
|
||||||
|
else
|
||||||
|
ratio = available[i];
|
||||||
|
|
||||||
|
bool okToSell = false;
|
||||||
|
|
||||||
|
if (i == GameResID::GOLD)
|
||||||
|
{
|
||||||
|
if (income[i] > 0 && !buildAnalyzer->isGoldPressureHigh())
|
||||||
|
okToSell = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (required[i] <= 0 && income[i] > 0)
|
||||||
|
okToSell = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ratio > maxRatio && okToSell) {
|
||||||
|
maxRatio = ratio;
|
||||||
|
mostExpendable = i;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#if NKAI_TRACE_LEVEL >= 2
|
||||||
|
logAi->debug("mostExpendable: %d mostWanted: %d", mostExpendable, mostWanted);
|
||||||
|
#endif
|
||||||
|
if (mostExpendable == mostWanted || mostWanted == -1 || mostExpendable == -1)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
int toGive;
|
||||||
|
int toGet;
|
||||||
|
m->getOffer(mostExpendable, mostWanted, toGive, toGet, EMarketMode::RESOURCE_RESOURCE);
|
||||||
|
//logAi->info("Offer is: I get %d of %s for %d of %s at %s", toGet, mostWanted, toGive, mostExpendable, obj->getObjectName());
|
||||||
|
//TODO trade only as much as needed
|
||||||
|
if (toGive && toGive <= available[mostExpendable]) //don't try to sell 0 resources
|
||||||
|
{
|
||||||
|
cb->trade(m->getObjInstanceID(), EMarketMode::RESOURCE_RESOURCE, GameResID(mostExpendable), GameResID(mostWanted), toGive);
|
||||||
|
#if NKAI_TRACE_LEVEL >= 1
|
||||||
|
logAi->info("Traded %d of %s for %d of %s at %s", toGive, mostExpendable, toGet, mostWanted, obj->getObjectName());
|
||||||
|
#endif
|
||||||
|
haveTraded = true;
|
||||||
|
shouldTryToTrade = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return haveTraded;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -120,13 +120,14 @@ public:
|
|||||||
ScanDepth getScanDepth() const { return scanDepth; }
|
ScanDepth getScanDepth() const { return scanDepth; }
|
||||||
bool isOpenMap() const { return openMap; }
|
bool isOpenMap() const { return openMap; }
|
||||||
bool isObjectGraphAllowed() const { return useObjectGraph; }
|
bool isObjectGraphAllowed() const { return useObjectGraph; }
|
||||||
|
bool handleTrading();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void resetAiState();
|
void resetAiState();
|
||||||
void updateAiState(int pass, bool fast = false);
|
void updateAiState(int pass, bool fast = false);
|
||||||
void decompose(Goals::TGoalVec & result, Goals::TSubgoal behavior, int decompositionMaxDepth) const;
|
void decompose(Goals::TGoalVec & result, Goals::TSubgoal behavior, int decompositionMaxDepth) const;
|
||||||
Goals::TTask choseBestTask(Goals::TGoalVec & tasks) const;
|
Goals::TTask choseBestTask(Goals::TGoalVec & tasks) const;
|
||||||
Goals::TTaskVec buildPlan(Goals::TGoalVec & tasks) const;
|
Goals::TTaskVec buildPlan(Goals::TGoalVec & tasks, int priorityTier) const;
|
||||||
bool executeTask(Goals::TTask task);
|
bool executeTask(Goals::TTask task);
|
||||||
bool areAffectedObjectsPresent(Goals::TTask task) const;
|
bool areAffectedObjectsPresent(Goals::TTask task) const;
|
||||||
HeroRole getTaskRole(Goals::TTask task) const;
|
HeroRole getTaskRole(Goals::TTask task) const;
|
||||||
|
@ -15,6 +15,8 @@
|
|||||||
#include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h"
|
#include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h"
|
||||||
#include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
|
#include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
|
||||||
#include "../../../lib/mapObjects/MapObjects.h"
|
#include "../../../lib/mapObjects/MapObjects.h"
|
||||||
|
#include "../../../lib/mapping/CMapDefines.h"
|
||||||
|
#include "../../../lib/RoadHandler.h"
|
||||||
#include "../../../lib/CCreatureHandler.h"
|
#include "../../../lib/CCreatureHandler.h"
|
||||||
#include "../../../lib/VCMI_Lib.h"
|
#include "../../../lib/VCMI_Lib.h"
|
||||||
#include "../../../lib/StartInfo.h"
|
#include "../../../lib/StartInfo.h"
|
||||||
@ -33,11 +35,9 @@
|
|||||||
namespace NKAI
|
namespace NKAI
|
||||||
{
|
{
|
||||||
|
|
||||||
#define MIN_AI_STRENGTH (0.5f) //lower when combat AI gets smarter
|
constexpr float MIN_CRITICAL_VALUE = 2.0f;
|
||||||
#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)
|
EvaluationContext::EvaluationContext(const Nullkiller* ai)
|
||||||
: movementCost(0.0),
|
: movementCost(0.0),
|
||||||
manaCost(0),
|
manaCost(0),
|
||||||
danger(0),
|
danger(0),
|
||||||
@ -51,9 +51,22 @@ EvaluationContext::EvaluationContext(const Nullkiller * ai)
|
|||||||
heroRole(HeroRole::SCOUT),
|
heroRole(HeroRole::SCOUT),
|
||||||
turn(0),
|
turn(0),
|
||||||
strategicalValue(0),
|
strategicalValue(0),
|
||||||
|
conquestValue(0),
|
||||||
evaluator(ai),
|
evaluator(ai),
|
||||||
enemyHeroDangerRatio(0),
|
enemyHeroDangerRatio(0),
|
||||||
armyGrowth(0)
|
threat(0),
|
||||||
|
armyGrowth(0),
|
||||||
|
armyInvolvement(0),
|
||||||
|
defenseValue(0),
|
||||||
|
isDefend(false),
|
||||||
|
threatTurns(INT_MAX),
|
||||||
|
involvesSailing(false),
|
||||||
|
isTradeBuilding(false),
|
||||||
|
isExchange(false),
|
||||||
|
isArmyUpgrade(false),
|
||||||
|
isHero(false),
|
||||||
|
isEnemy(false),
|
||||||
|
explorePriority(0)
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -225,7 +238,7 @@ int getDwellingArmyCost(const CGObjectInstance * target)
|
|||||||
auto creature = creLevel.second.back().toCreature();
|
auto creature = creLevel.second.back().toCreature();
|
||||||
auto creaturesAreFree = creature->getLevel() == 1;
|
auto creaturesAreFree = creature->getLevel() == 1;
|
||||||
if(!creaturesAreFree)
|
if(!creaturesAreFree)
|
||||||
cost += creature->getRecruitCost(EGameResID::GOLD) * creLevel.first;
|
cost += creature->getFullRecruitCost().marketValue() * creLevel.first;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -251,6 +264,8 @@ static uint64_t evaluateArtifactArmyValue(const CArtifact * art)
|
|||||||
|
|
||||||
switch(art->aClass)
|
switch(art->aClass)
|
||||||
{
|
{
|
||||||
|
case CArtifact::EartClass::ART_TREASURE:
|
||||||
|
//FALL_THROUGH
|
||||||
case CArtifact::EartClass::ART_MINOR:
|
case CArtifact::EartClass::ART_MINOR:
|
||||||
classValue = 1000;
|
classValue = 1000;
|
||||||
break;
|
break;
|
||||||
@ -289,6 +304,8 @@ uint64_t RewardEvaluator::getArmyReward(
|
|||||||
case Obj::CREATURE_GENERATOR3:
|
case Obj::CREATURE_GENERATOR3:
|
||||||
case Obj::CREATURE_GENERATOR4:
|
case Obj::CREATURE_GENERATOR4:
|
||||||
return getDwellingArmyValue(ai->cb.get(), target, checkGold);
|
return getDwellingArmyValue(ai->cb.get(), target, checkGold);
|
||||||
|
case Obj::SPELL_SCROLL:
|
||||||
|
//FALL_THROUGH
|
||||||
case Obj::ARTIFACT:
|
case Obj::ARTIFACT:
|
||||||
return evaluateArtifactArmyValue(dynamic_cast<const CGArtifact *>(target)->storedArtifact->getType());
|
return evaluateArtifactArmyValue(dynamic_cast<const CGArtifact *>(target)->storedArtifact->getType());
|
||||||
case Obj::HERO:
|
case Obj::HERO:
|
||||||
@ -479,7 +496,7 @@ uint64_t RewardEvaluator::townArmyGrowth(const CGTownInstance * town) const
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64_t RewardEvaluator::getManaRecoveryArmyReward(const CGHeroInstance * hero) const
|
float RewardEvaluator::getManaRecoveryArmyReward(const CGHeroInstance * hero) const
|
||||||
{
|
{
|
||||||
return ai->heroManager->getMagicStrength(hero) * 10000 * (1.0f - std::sqrt(static_cast<float>(hero->mana) / hero->manaLimit()));
|
return ai->heroManager->getMagicStrength(hero) * 10000 * (1.0f - std::sqrt(static_cast<float>(hero->mana) / hero->manaLimit()));
|
||||||
}
|
}
|
||||||
@ -581,6 +598,54 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target, cons
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
float RewardEvaluator::getConquestValue(const CGObjectInstance* target) const
|
||||||
|
{
|
||||||
|
if (!target)
|
||||||
|
return 0;
|
||||||
|
if (target->getOwner() == ai->playerID)
|
||||||
|
return 0;
|
||||||
|
switch (target->ID)
|
||||||
|
{
|
||||||
|
case Obj::TOWN:
|
||||||
|
{
|
||||||
|
if (ai->buildAnalyzer->getDevelopmentInfo().empty())
|
||||||
|
return 10.0f;
|
||||||
|
|
||||||
|
auto town = dynamic_cast<const CGTownInstance*>(target);
|
||||||
|
|
||||||
|
if (town->getOwner() == ai->playerID)
|
||||||
|
{
|
||||||
|
auto armyIncome = townArmyGrowth(town);
|
||||||
|
auto dailyIncome = town->dailyIncome()[EGameResID::GOLD];
|
||||||
|
|
||||||
|
return std::min(1.0f, std::sqrt(armyIncome / 40000.0f)) + std::min(0.3f, dailyIncome / 10000.0f);
|
||||||
|
}
|
||||||
|
|
||||||
|
auto fortLevel = town->fortLevel();
|
||||||
|
auto booster = 1.0f;
|
||||||
|
|
||||||
|
if (town->hasCapitol())
|
||||||
|
return booster * 1.5;
|
||||||
|
|
||||||
|
if (fortLevel < CGTownInstance::CITADEL)
|
||||||
|
return booster * (town->hasFort() ? 1.0 : 0.8);
|
||||||
|
else
|
||||||
|
return booster * (fortLevel == CGTownInstance::CASTLE ? 1.4 : 1.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
case Obj::HERO:
|
||||||
|
return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES
|
||||||
|
? getEnemyHeroStrategicalValue(dynamic_cast<const CGHeroInstance*>(target))
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
case Obj::KEYMASTER:
|
||||||
|
return 0.6f;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
float RewardEvaluator::evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const
|
float RewardEvaluator::evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const
|
||||||
{
|
{
|
||||||
auto rewardable = dynamic_cast<const CRewardableObject *>(hut);
|
auto rewardable = dynamic_cast<const CRewardableObject *>(hut);
|
||||||
@ -705,7 +770,7 @@ int32_t getArmyCost(const CArmedInstance * army)
|
|||||||
|
|
||||||
for(auto stack : army->Slots())
|
for(auto stack : army->Slots())
|
||||||
{
|
{
|
||||||
value += stack.second->getCreatureID().toCreature()->getRecruitCost(EGameResID::GOLD) * stack.second->count;
|
value += stack.second->getCreatureID().toCreature()->getFullRecruitCost().marketValue() * stack.second->count;
|
||||||
}
|
}
|
||||||
|
|
||||||
return value;
|
return value;
|
||||||
@ -786,7 +851,9 @@ public:
|
|||||||
uint64_t armyStrength = heroExchange.getReinforcementArmyStrength(evaluationContext.evaluator.ai);
|
uint64_t armyStrength = heroExchange.getReinforcementArmyStrength(evaluationContext.evaluator.ai);
|
||||||
|
|
||||||
evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength());
|
evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength());
|
||||||
|
evaluationContext.conquestValue += 2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength();
|
||||||
evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero);
|
evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero);
|
||||||
|
evaluationContext.isExchange = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -804,6 +871,7 @@ public:
|
|||||||
|
|
||||||
evaluationContext.armyReward += upgradeValue;
|
evaluationContext.armyReward += upgradeValue;
|
||||||
evaluationContext.addNonCriticalStrategicalValue(upgradeValue / (float)armyUpgrade.hero->getArmyStrength());
|
evaluationContext.addNonCriticalStrategicalValue(upgradeValue / (float)armyUpgrade.hero->getArmyStrength());
|
||||||
|
evaluationContext.isArmyUpgrade = true;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -818,22 +886,46 @@ public:
|
|||||||
int tilesDiscovered = task->value;
|
int tilesDiscovered = task->value;
|
||||||
|
|
||||||
evaluationContext.addNonCriticalStrategicalValue(0.03f * tilesDiscovered);
|
evaluationContext.addNonCriticalStrategicalValue(0.03f * tilesDiscovered);
|
||||||
|
for (auto obj : evaluationContext.evaluator.ai->cb->getVisitableObjs(task->tile))
|
||||||
|
{
|
||||||
|
switch (obj->ID.num)
|
||||||
|
{
|
||||||
|
case Obj::MONOLITH_ONE_WAY_ENTRANCE:
|
||||||
|
case Obj::MONOLITH_TWO_WAY:
|
||||||
|
case Obj::SUBTERRANEAN_GATE:
|
||||||
|
evaluationContext.explorePriority = 1;
|
||||||
|
break;
|
||||||
|
case Obj::REDWOOD_OBSERVATORY:
|
||||||
|
case Obj::PILLAR_OF_FIRE:
|
||||||
|
evaluationContext.explorePriority = 2;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(evaluationContext.evaluator.ai->cb->getTile(task->tile)->roadType != RoadId::NO_ROAD)
|
||||||
|
evaluationContext.explorePriority = 1;
|
||||||
|
if (evaluationContext.explorePriority == 0)
|
||||||
|
evaluationContext.explorePriority = 3;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class StayAtTownManaRecoveryEvaluator : public IEvaluationContextBuilder
|
class StayAtTownManaRecoveryEvaluator : public IEvaluationContextBuilder
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
|
void buildEvaluationContext(EvaluationContext& evaluationContext, Goals::TSubgoal task) const override
|
||||||
{
|
{
|
||||||
if(task->goalType != Goals::STAY_AT_TOWN)
|
if (task->goalType != Goals::STAY_AT_TOWN)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
Goals::StayAtTown & stayAtTown = dynamic_cast<Goals::StayAtTown &>(*task);
|
Goals::StayAtTown& stayAtTown = dynamic_cast<Goals::StayAtTown&>(*task);
|
||||||
|
|
||||||
evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero());
|
evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero());
|
||||||
evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted();
|
if (evaluationContext.armyReward == 0)
|
||||||
evaluationContext.movementCost += stayAtTown.getMovementWasted();
|
evaluationContext.isDefend = true;
|
||||||
|
else
|
||||||
|
{
|
||||||
|
evaluationContext.movementCost += stayAtTown.getMovementWasted();
|
||||||
|
evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -844,15 +936,8 @@ void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uin
|
|||||||
if(enemyDanger.danger)
|
if(enemyDanger.danger)
|
||||||
{
|
{
|
||||||
auto dangerRatio = enemyDanger.danger / (double)ourStrength;
|
auto dangerRatio = enemyDanger.danger / (double)ourStrength;
|
||||||
auto enemyHero = evaluationContext.evaluator.ai->cb->getObj(enemyDanger.hero.hid, false);
|
|
||||||
bool isAI = enemyHero && isAnotherAi(enemyHero, *evaluationContext.evaluator.ai->cb);
|
|
||||||
|
|
||||||
if(isAI)
|
|
||||||
{
|
|
||||||
dangerRatio *= 1.5; // lets make AI bit more afraid of other AI.
|
|
||||||
}
|
|
||||||
|
|
||||||
vstd::amax(evaluationContext.enemyHeroDangerRatio, dangerRatio);
|
vstd::amax(evaluationContext.enemyHeroDangerRatio, dangerRatio);
|
||||||
|
vstd::amax(evaluationContext.threat, enemyDanger.threat);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -896,6 +981,10 @@ public:
|
|||||||
else
|
else
|
||||||
evaluationContext.addNonCriticalStrategicalValue(1.7f * multiplier * strategicalValue);
|
evaluationContext.addNonCriticalStrategicalValue(1.7f * multiplier * strategicalValue);
|
||||||
|
|
||||||
|
evaluationContext.defenseValue = town->fortLevel();
|
||||||
|
evaluationContext.isDefend = true;
|
||||||
|
evaluationContext.threatTurns = treat.turn;
|
||||||
|
|
||||||
vstd::amax(evaluationContext.danger, defendTown.getTreat().danger);
|
vstd::amax(evaluationContext.danger, defendTown.getTreat().danger);
|
||||||
addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength());
|
addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength());
|
||||||
}
|
}
|
||||||
@ -926,6 +1015,8 @@ public:
|
|||||||
for(auto & node : path.nodes)
|
for(auto & node : path.nodes)
|
||||||
{
|
{
|
||||||
vstd::amax(costsPerHero[node.targetHero], node.cost);
|
vstd::amax(costsPerHero[node.targetHero], node.cost);
|
||||||
|
if (node.layer == EPathfindingLayer::SAIL)
|
||||||
|
evaluationContext.involvesSailing = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
for(auto pair : costsPerHero)
|
for(auto pair : costsPerHero)
|
||||||
@ -952,10 +1043,18 @@ public:
|
|||||||
evaluationContext.armyGrowth += evaluationContext.evaluator.getArmyGrowth(target, hero, army);
|
evaluationContext.armyGrowth += evaluationContext.evaluator.getArmyGrowth(target, hero, army);
|
||||||
evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, heroRole);
|
evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, heroRole);
|
||||||
evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target));
|
evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target));
|
||||||
|
evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target);
|
||||||
|
if (target->ID == Obj::HERO)
|
||||||
|
evaluationContext.isHero = true;
|
||||||
|
if (target->getOwner() != PlayerColor::NEUTRAL && ai->cb->getPlayerRelations(ai->playerID, target->getOwner()) == PlayerRelations::ENEMIES)
|
||||||
|
evaluationContext.isEnemy = true;
|
||||||
evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army);
|
evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army);
|
||||||
|
evaluationContext.armyInvolvement += army->getArmyCost();
|
||||||
|
if(evaluationContext.danger > 0)
|
||||||
|
evaluationContext.skillReward += (float)evaluationContext.danger / (float)hero->getArmyStrength();
|
||||||
}
|
}
|
||||||
|
|
||||||
vstd::amax(evaluationContext.armyLossPersentage, path.getTotalArmyLoss() / (double)path.getHeroStrength());
|
vstd::amax(evaluationContext.armyLossPersentage, (float)path.getTotalArmyLoss() / (float)army->getArmyStrength());
|
||||||
addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength());
|
addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength());
|
||||||
vstd::amax(evaluationContext.turn, path.turn());
|
vstd::amax(evaluationContext.turn, path.turn());
|
||||||
}
|
}
|
||||||
@ -996,6 +1095,7 @@ public:
|
|||||||
evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold) / boost;
|
evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold) / boost;
|
||||||
evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, role) / boost;
|
evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, role) / boost;
|
||||||
evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target) / boost);
|
evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target) / boost);
|
||||||
|
evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target);
|
||||||
evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army) / boost;
|
evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army) / boost;
|
||||||
evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost;
|
evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost;
|
||||||
evaluationContext.movementCost += objInfo.second.movementCost / boost;
|
evaluationContext.movementCost += objInfo.second.movementCost / boost;
|
||||||
@ -1021,6 +1121,14 @@ public:
|
|||||||
Goals::ExchangeSwapTownHeroes & swapCommand = dynamic_cast<Goals::ExchangeSwapTownHeroes &>(*task);
|
Goals::ExchangeSwapTownHeroes & swapCommand = dynamic_cast<Goals::ExchangeSwapTownHeroes &>(*task);
|
||||||
const CGHeroInstance * garrisonHero = swapCommand.getGarrisonHero();
|
const CGHeroInstance * garrisonHero = swapCommand.getGarrisonHero();
|
||||||
|
|
||||||
|
logAi->trace("buildEvaluationContext ExchangeSwapTownHeroesContextBuilder %s affected objects: %d", swapCommand.toString(), swapCommand.getAffectedObjects().size());
|
||||||
|
for (auto obj : swapCommand.getAffectedObjects())
|
||||||
|
{
|
||||||
|
logAi->trace("affected object: %s", evaluationContext.evaluator.ai->cb->getObj(obj)->getObjectName());
|
||||||
|
}
|
||||||
|
if (garrisonHero)
|
||||||
|
logAi->debug("with %s and %d", garrisonHero->getNameTranslated(), int(swapCommand.getLockingReason()));
|
||||||
|
|
||||||
if(garrisonHero && swapCommand.getLockingReason() == HeroLockedReason::DEFENCE)
|
if(garrisonHero && swapCommand.getLockingReason() == HeroLockedReason::DEFENCE)
|
||||||
{
|
{
|
||||||
auto defenderRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(garrisonHero);
|
auto defenderRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(garrisonHero);
|
||||||
@ -1029,6 +1137,9 @@ public:
|
|||||||
evaluationContext.movementCost += mpLeft;
|
evaluationContext.movementCost += mpLeft;
|
||||||
evaluationContext.movementCostByRole[defenderRole] += mpLeft;
|
evaluationContext.movementCostByRole[defenderRole] += mpLeft;
|
||||||
evaluationContext.heroRole = defenderRole;
|
evaluationContext.heroRole = defenderRole;
|
||||||
|
evaluationContext.isDefend = true;
|
||||||
|
evaluationContext.armyInvolvement = garrisonHero->getArmyStrength();
|
||||||
|
logAi->debug("evaluationContext.isDefend: %d", evaluationContext.isDefend);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -1072,8 +1183,14 @@ public:
|
|||||||
evaluationContext.goldReward += 7 * bi.dailyIncome[EGameResID::GOLD] / 2; // 7 day income but half we already have
|
evaluationContext.goldReward += 7 * bi.dailyIncome[EGameResID::GOLD] / 2; // 7 day income but half we already have
|
||||||
evaluationContext.heroRole = HeroRole::MAIN;
|
evaluationContext.heroRole = HeroRole::MAIN;
|
||||||
evaluationContext.movementCostByRole[evaluationContext.heroRole] += bi.prerequisitesCount;
|
evaluationContext.movementCostByRole[evaluationContext.heroRole] += bi.prerequisitesCount;
|
||||||
evaluationContext.goldCost += bi.buildCostWithPrerequisites[EGameResID::GOLD];
|
int32_t cost = bi.buildCost[EGameResID::GOLD];
|
||||||
|
evaluationContext.goldCost += cost;
|
||||||
evaluationContext.closestWayRatio = 1;
|
evaluationContext.closestWayRatio = 1;
|
||||||
|
evaluationContext.buildingCost += bi.buildCostWithPrerequisites;
|
||||||
|
if (bi.id == BuildingID::MARKETPLACE || bi.dailyIncome[EGameResID::WOOD] > 0)
|
||||||
|
evaluationContext.isTradeBuilding = true;
|
||||||
|
|
||||||
|
logAi->trace("Building costs for %s : %s MarketValue: %d",bi.toString(), evaluationContext.buildingCost.toString(), evaluationContext.buildingCost.marketValue());
|
||||||
|
|
||||||
if(bi.creatureID != CreatureID::NONE)
|
if(bi.creatureID != CreatureID::NONE)
|
||||||
{
|
{
|
||||||
@ -1100,7 +1217,18 @@ public:
|
|||||||
else if(bi.id >= BuildingID::MAGES_GUILD_1 && bi.id <= BuildingID::MAGES_GUILD_5)
|
else if(bi.id >= BuildingID::MAGES_GUILD_1 && bi.id <= BuildingID::MAGES_GUILD_5)
|
||||||
{
|
{
|
||||||
evaluationContext.skillReward += 2 * (bi.id - BuildingID::MAGES_GUILD_1);
|
evaluationContext.skillReward += 2 * (bi.id - BuildingID::MAGES_GUILD_1);
|
||||||
|
for (auto hero : evaluationContext.evaluator.ai->cb->getHeroesInfo())
|
||||||
|
{
|
||||||
|
evaluationContext.armyInvolvement += hero->getArmyCost();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
int sameTownBonus = 0;
|
||||||
|
for (auto town : evaluationContext.evaluator.ai->cb->getTownsInfo())
|
||||||
|
{
|
||||||
|
if (buildThis.town->getFaction() == town->getFaction())
|
||||||
|
sameTownBonus += town->getTownLevel();
|
||||||
|
}
|
||||||
|
evaluationContext.armyReward *= sameTownBonus;
|
||||||
|
|
||||||
if(evaluationContext.goldReward)
|
if(evaluationContext.goldReward)
|
||||||
{
|
{
|
||||||
@ -1162,6 +1290,7 @@ EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal
|
|||||||
for(auto subgoal : parts)
|
for(auto subgoal : parts)
|
||||||
{
|
{
|
||||||
context.goldCost += subgoal->goldCost;
|
context.goldCost += subgoal->goldCost;
|
||||||
|
context.buildingCost += subgoal->buildingCost;
|
||||||
|
|
||||||
for(auto builder : evaluationContextBuilders)
|
for(auto builder : evaluationContextBuilders)
|
||||||
{
|
{
|
||||||
@ -1172,7 +1301,7 @@ EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal
|
|||||||
return context;
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
float PriorityEvaluator::evaluate(Goals::TSubgoal task)
|
float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
|
||||||
{
|
{
|
||||||
auto evaluationContext = buildEvaluationContext(task);
|
auto evaluationContext = buildEvaluationContext(task);
|
||||||
|
|
||||||
@ -1185,36 +1314,257 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
|
|||||||
|
|
||||||
double result = 0;
|
double result = 0;
|
||||||
|
|
||||||
try
|
if (ai->settings->isUseFuzzy())
|
||||||
{
|
{
|
||||||
armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage);
|
float fuzzyResult = 0;
|
||||||
heroRoleVariable->setValue(evaluationContext.heroRole);
|
try
|
||||||
mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]);
|
{
|
||||||
scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]);
|
armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage);
|
||||||
goldRewardVariable->setValue(goldRewardPerTurn);
|
heroRoleVariable->setValue(evaluationContext.heroRole);
|
||||||
armyRewardVariable->setValue(evaluationContext.armyReward);
|
mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]);
|
||||||
armyGrowthVariable->setValue(evaluationContext.armyGrowth);
|
scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]);
|
||||||
skillRewardVariable->setValue(evaluationContext.skillReward);
|
goldRewardVariable->setValue(goldRewardPerTurn);
|
||||||
dangerVariable->setValue(evaluationContext.danger);
|
armyRewardVariable->setValue(evaluationContext.armyReward);
|
||||||
rewardTypeVariable->setValue(rewardType);
|
armyGrowthVariable->setValue(evaluationContext.armyGrowth);
|
||||||
closestHeroRatioVariable->setValue(evaluationContext.closestWayRatio);
|
skillRewardVariable->setValue(evaluationContext.skillReward);
|
||||||
strategicalValueVariable->setValue(evaluationContext.strategicalValue);
|
dangerVariable->setValue(evaluationContext.danger);
|
||||||
goldPressureVariable->setValue(ai->buildAnalyzer->getGoldPressure());
|
rewardTypeVariable->setValue(rewardType);
|
||||||
goldCostVariable->setValue(evaluationContext.goldCost / ((float)ai->getFreeResources()[EGameResID::GOLD] + (float)ai->buildAnalyzer->getDailyIncome()[EGameResID::GOLD] + 1.0f));
|
closestHeroRatioVariable->setValue(evaluationContext.closestWayRatio);
|
||||||
turnVariable->setValue(evaluationContext.turn);
|
strategicalValueVariable->setValue(evaluationContext.strategicalValue);
|
||||||
fearVariable->setValue(evaluationContext.enemyHeroDangerRatio);
|
goldPressureVariable->setValue(ai->buildAnalyzer->getGoldPressure());
|
||||||
|
goldCostVariable->setValue(evaluationContext.goldCost / ((float)ai->getFreeResources()[EGameResID::GOLD] + (float)ai->buildAnalyzer->getDailyIncome()[EGameResID::GOLD] + 1.0f));
|
||||||
|
turnVariable->setValue(evaluationContext.turn);
|
||||||
|
fearVariable->setValue(evaluationContext.enemyHeroDangerRatio);
|
||||||
|
|
||||||
engine->process();
|
engine->process();
|
||||||
|
|
||||||
result = value->getValue();
|
fuzzyResult = value->getValue();
|
||||||
|
}
|
||||||
|
catch (fl::Exception& fe)
|
||||||
|
{
|
||||||
|
logAi->error("evaluate VisitTile: %s", fe.getWhat());
|
||||||
|
}
|
||||||
|
result = fuzzyResult;
|
||||||
}
|
}
|
||||||
catch(fl::Exception & fe)
|
else
|
||||||
{
|
{
|
||||||
logAi->error("evaluate VisitTile: %s", fe.getWhat());
|
float score = 0;
|
||||||
|
const bool amIInDanger = ai->cb->getTownsInfo().empty() || (evaluationContext.isDefend && evaluationContext.threatTurns == 0);
|
||||||
|
const float maxWillingToLose = amIInDanger ? 1 : ai->settings->getMaxArmyLossTarget();
|
||||||
|
|
||||||
|
bool arriveNextWeek = false;
|
||||||
|
if (ai->cb->getDate(Date::DAY_OF_WEEK) + evaluationContext.turn > 7)
|
||||||
|
arriveNextWeek = true;
|
||||||
|
|
||||||
|
#if NKAI_TRACE_LEVEL >= 2
|
||||||
|
logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, explorePriority: %d isDefend: %d",
|
||||||
|
priorityTier,
|
||||||
|
task->toString(),
|
||||||
|
evaluationContext.armyLossPersentage,
|
||||||
|
(int)evaluationContext.turn,
|
||||||
|
evaluationContext.movementCostByRole[HeroRole::MAIN],
|
||||||
|
evaluationContext.movementCostByRole[HeroRole::SCOUT],
|
||||||
|
goldRewardPerTurn,
|
||||||
|
evaluationContext.goldCost,
|
||||||
|
evaluationContext.armyReward,
|
||||||
|
evaluationContext.armyGrowth,
|
||||||
|
evaluationContext.skillReward,
|
||||||
|
evaluationContext.danger,
|
||||||
|
evaluationContext.threatTurns,
|
||||||
|
evaluationContext.threat,
|
||||||
|
evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout",
|
||||||
|
evaluationContext.strategicalValue,
|
||||||
|
evaluationContext.conquestValue,
|
||||||
|
evaluationContext.closestWayRatio,
|
||||||
|
evaluationContext.enemyHeroDangerRatio,
|
||||||
|
evaluationContext.explorePriority,
|
||||||
|
evaluationContext.isDefend);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
switch (priorityTier)
|
||||||
|
{
|
||||||
|
case PriorityTier::INSTAKILL: //Take towns / kill heroes in immediate reach
|
||||||
|
{
|
||||||
|
if (evaluationContext.turn > 0)
|
||||||
|
return 0;
|
||||||
|
if(evaluationContext.conquestValue > 0)
|
||||||
|
score = 1000;
|
||||||
|
if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty()))
|
||||||
|
return 0;
|
||||||
|
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
|
||||||
|
return 0;
|
||||||
|
score *= evaluationContext.closestWayRatio;
|
||||||
|
if (evaluationContext.movementCost > 0)
|
||||||
|
score /= evaluationContext.movementCost;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PriorityTier::INSTADEFEND: //Defend immediately threatened towns
|
||||||
|
{
|
||||||
|
if (evaluationContext.isDefend && evaluationContext.threatTurns == 0 && evaluationContext.turn == 0)
|
||||||
|
score = evaluationContext.armyInvolvement;
|
||||||
|
if (evaluationContext.isEnemy && maxWillingToLose - evaluationContext.armyLossPersentage < 0)
|
||||||
|
return 0;
|
||||||
|
score *= evaluationContext.closestWayRatio;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PriorityTier::KILL: //Take towns / kill heroes that are further away
|
||||||
|
{
|
||||||
|
if (evaluationContext.turn > 0 && evaluationContext.isHero)
|
||||||
|
return 0;
|
||||||
|
if (arriveNextWeek && evaluationContext.isEnemy)
|
||||||
|
return 0;
|
||||||
|
if (evaluationContext.conquestValue > 0)
|
||||||
|
score = 1000;
|
||||||
|
if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty()))
|
||||||
|
return 0;
|
||||||
|
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
|
||||||
|
return 0;
|
||||||
|
score *= evaluationContext.closestWayRatio;
|
||||||
|
if (evaluationContext.movementCost > 0)
|
||||||
|
score /= evaluationContext.movementCost;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PriorityTier::UPGRADE:
|
||||||
|
{
|
||||||
|
if (!evaluationContext.isArmyUpgrade)
|
||||||
|
return 0;
|
||||||
|
if (evaluationContext.enemyHeroDangerRatio > 1)
|
||||||
|
return 0;
|
||||||
|
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
|
||||||
|
return 0;
|
||||||
|
score = 1000;
|
||||||
|
score *= evaluationContext.closestWayRatio;
|
||||||
|
if (evaluationContext.movementCost > 0)
|
||||||
|
score /= evaluationContext.movementCost;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PriorityTier::HIGH_PRIO_EXPLORE:
|
||||||
|
{
|
||||||
|
if (evaluationContext.enemyHeroDangerRatio > 1)
|
||||||
|
return 0;
|
||||||
|
if (evaluationContext.explorePriority != 1)
|
||||||
|
return 0;
|
||||||
|
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
|
||||||
|
return 0;
|
||||||
|
score = 1000;
|
||||||
|
score *= evaluationContext.closestWayRatio;
|
||||||
|
if (evaluationContext.movementCost > 0)
|
||||||
|
score /= evaluationContext.movementCost;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PriorityTier::HUNTER_GATHER: //Collect guarded stuff
|
||||||
|
{
|
||||||
|
if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend)
|
||||||
|
return 0;
|
||||||
|
if (evaluationContext.buildingCost.marketValue() > 0)
|
||||||
|
return 0;
|
||||||
|
if (evaluationContext.isDefend && (evaluationContext.enemyHeroDangerRatio < 1 || evaluationContext.threatTurns > 0 || evaluationContext.turn > 0))
|
||||||
|
return 0;
|
||||||
|
if (evaluationContext.explorePriority == 3)
|
||||||
|
return 0;
|
||||||
|
if (evaluationContext.isArmyUpgrade)
|
||||||
|
return 0;
|
||||||
|
if ((evaluationContext.enemyHeroDangerRatio > 0 && arriveNextWeek) || evaluationContext.enemyHeroDangerRatio > 1)
|
||||||
|
return 0;
|
||||||
|
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
|
||||||
|
return 0;
|
||||||
|
score += evaluationContext.strategicalValue * 1000;
|
||||||
|
score += evaluationContext.goldReward;
|
||||||
|
score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05;
|
||||||
|
score += evaluationContext.armyReward;
|
||||||
|
score += evaluationContext.armyGrowth;
|
||||||
|
score -= evaluationContext.goldCost;
|
||||||
|
score -= evaluationContext.armyInvolvement * evaluationContext.armyLossPersentage;
|
||||||
|
if (score > 0)
|
||||||
|
{
|
||||||
|
score = 1000;
|
||||||
|
score *= evaluationContext.closestWayRatio;
|
||||||
|
if (evaluationContext.movementCost > 0)
|
||||||
|
score /= evaluationContext.movementCost;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PriorityTier::LOW_PRIO_EXPLORE:
|
||||||
|
{
|
||||||
|
if (evaluationContext.enemyHeroDangerRatio > 1)
|
||||||
|
return 0;
|
||||||
|
if (evaluationContext.explorePriority != 3)
|
||||||
|
return 0;
|
||||||
|
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
|
||||||
|
return 0;
|
||||||
|
score = 1000;
|
||||||
|
score *= evaluationContext.closestWayRatio;
|
||||||
|
if (evaluationContext.movementCost > 0)
|
||||||
|
score /= evaluationContext.movementCost;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PriorityTier::DEFEND: //Defend whatever if nothing else is to do
|
||||||
|
{
|
||||||
|
if (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.isExchange)
|
||||||
|
return 0;
|
||||||
|
if (evaluationContext.isDefend || evaluationContext.isArmyUpgrade)
|
||||||
|
score = 1000;
|
||||||
|
score *= evaluationContext.closestWayRatio;
|
||||||
|
score /= (evaluationContext.turn + 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case PriorityTier::BUILDINGS: //For buildings and buying army
|
||||||
|
{
|
||||||
|
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
|
||||||
|
return 0;
|
||||||
|
//If we already have locked resources, we don't look at other buildings
|
||||||
|
if (ai->getLockedResources().marketValue() > 0)
|
||||||
|
return 0;
|
||||||
|
score += evaluationContext.conquestValue * 1000;
|
||||||
|
score += evaluationContext.strategicalValue * 1000;
|
||||||
|
score += evaluationContext.goldReward;
|
||||||
|
score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05;
|
||||||
|
score += evaluationContext.armyReward;
|
||||||
|
score += evaluationContext.armyGrowth;
|
||||||
|
if (evaluationContext.buildingCost.marketValue() > 0)
|
||||||
|
{
|
||||||
|
if (!evaluationContext.isTradeBuilding && ai->getFreeResources()[EGameResID::WOOD] - evaluationContext.buildingCost[EGameResID::WOOD] < 5 && ai->buildAnalyzer->getDailyIncome()[EGameResID::WOOD] < 1)
|
||||||
|
{
|
||||||
|
logAi->trace("Should make sure to build market-place instead of %s", task->toString());
|
||||||
|
for (auto town : ai->cb->getTownsInfo())
|
||||||
|
{
|
||||||
|
if (!town->hasBuiltSomeTradeBuilding())
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
score += 1000;
|
||||||
|
auto resourcesAvailable = evaluationContext.evaluator.ai->getFreeResources();
|
||||||
|
auto income = ai->buildAnalyzer->getDailyIncome();
|
||||||
|
if(ai->buildAnalyzer->isGoldPressureHigh())
|
||||||
|
score /= evaluationContext.buildingCost.marketValue();
|
||||||
|
if (!resourcesAvailable.canAfford(evaluationContext.buildingCost))
|
||||||
|
{
|
||||||
|
TResources needed = evaluationContext.buildingCost - resourcesAvailable;
|
||||||
|
needed.positive();
|
||||||
|
int turnsTo = needed.maxPurchasableCount(income);
|
||||||
|
if (turnsTo == INT_MAX)
|
||||||
|
return 0;
|
||||||
|
else
|
||||||
|
score /= turnsTo;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend && vstd::isAlmostZero(evaluationContext.conquestValue))
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result = score;
|
||||||
|
//TODO: Figure out the root cause for why evaluationContext.closestWayRatio has become -nan(ind).
|
||||||
|
if (std::isnan(result))
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
#if NKAI_TRACE_LEVEL >= 2
|
#if NKAI_TRACE_LEVEL >= 2
|
||||||
logAi->trace("Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, danger: %d, role: %s, strategical value: %f, cwr: %f, fear: %f, result %f",
|
logAi->trace("priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, result %f",
|
||||||
|
priorityTier,
|
||||||
task->toString(),
|
task->toString(),
|
||||||
evaluationContext.armyLossPersentage,
|
evaluationContext.armyLossPersentage,
|
||||||
(int)evaluationContext.turn,
|
(int)evaluationContext.turn,
|
||||||
@ -1223,9 +1573,14 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
|
|||||||
goldRewardPerTurn,
|
goldRewardPerTurn,
|
||||||
evaluationContext.goldCost,
|
evaluationContext.goldCost,
|
||||||
evaluationContext.armyReward,
|
evaluationContext.armyReward,
|
||||||
|
evaluationContext.armyGrowth,
|
||||||
|
evaluationContext.skillReward,
|
||||||
evaluationContext.danger,
|
evaluationContext.danger,
|
||||||
|
evaluationContext.threatTurns,
|
||||||
|
evaluationContext.threat,
|
||||||
evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout",
|
evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout",
|
||||||
evaluationContext.strategicalValue,
|
evaluationContext.strategicalValue,
|
||||||
|
evaluationContext.conquestValue,
|
||||||
evaluationContext.closestWayRatio,
|
evaluationContext.closestWayRatio,
|
||||||
evaluationContext.enemyHeroDangerRatio,
|
evaluationContext.enemyHeroDangerRatio,
|
||||||
result);
|
result);
|
||||||
|
@ -41,6 +41,7 @@ public:
|
|||||||
float getResourceRequirementStrength(int resType) const;
|
float getResourceRequirementStrength(int resType) const;
|
||||||
float getResourceRequirementStrength(const TResources & res) const;
|
float getResourceRequirementStrength(const TResources & res) const;
|
||||||
float getStrategicalValue(const CGObjectInstance * target, const CGHeroInstance * hero = nullptr) const;
|
float getStrategicalValue(const CGObjectInstance * target, const CGHeroInstance * hero = nullptr) const;
|
||||||
|
float getConquestValue(const CGObjectInstance* target) const;
|
||||||
float getTotalResourceRequirementStrength(int resType) const;
|
float getTotalResourceRequirementStrength(int resType) const;
|
||||||
float evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const;
|
float evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const;
|
||||||
float getSkillReward(const CGObjectInstance * target, const CGHeroInstance * hero, HeroRole role) const;
|
float getSkillReward(const CGObjectInstance * target, const CGHeroInstance * hero, HeroRole role) const;
|
||||||
@ -48,7 +49,7 @@ public:
|
|||||||
uint64_t getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const;
|
uint64_t getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const;
|
||||||
const HitMapInfo & getEnemyHeroDanger(const int3 & tile, uint8_t turn) const;
|
const HitMapInfo & getEnemyHeroDanger(const int3 & tile, uint8_t turn) const;
|
||||||
uint64_t townArmyGrowth(const CGTownInstance * town) const;
|
uint64_t townArmyGrowth(const CGTownInstance * town) const;
|
||||||
uint64_t getManaRecoveryArmyReward(const CGHeroInstance * hero) const;
|
float getManaRecoveryArmyReward(const CGHeroInstance * hero) const;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct DLL_EXPORT EvaluationContext
|
struct DLL_EXPORT EvaluationContext
|
||||||
@ -65,10 +66,24 @@ struct DLL_EXPORT EvaluationContext
|
|||||||
int32_t goldCost;
|
int32_t goldCost;
|
||||||
float skillReward;
|
float skillReward;
|
||||||
float strategicalValue;
|
float strategicalValue;
|
||||||
|
float conquestValue;
|
||||||
HeroRole heroRole;
|
HeroRole heroRole;
|
||||||
uint8_t turn;
|
uint8_t turn;
|
||||||
RewardEvaluator evaluator;
|
RewardEvaluator evaluator;
|
||||||
float enemyHeroDangerRatio;
|
float enemyHeroDangerRatio;
|
||||||
|
float threat;
|
||||||
|
float armyInvolvement;
|
||||||
|
int defenseValue;
|
||||||
|
bool isDefend;
|
||||||
|
int threatTurns;
|
||||||
|
TResources buildingCost;
|
||||||
|
bool involvesSailing;
|
||||||
|
bool isTradeBuilding;
|
||||||
|
bool isExchange;
|
||||||
|
bool isArmyUpgrade;
|
||||||
|
bool isHero;
|
||||||
|
bool isEnemy;
|
||||||
|
int explorePriority;
|
||||||
|
|
||||||
EvaluationContext(const Nullkiller * ai);
|
EvaluationContext(const Nullkiller * ai);
|
||||||
|
|
||||||
@ -91,7 +106,20 @@ public:
|
|||||||
~PriorityEvaluator();
|
~PriorityEvaluator();
|
||||||
void initVisitTile();
|
void initVisitTile();
|
||||||
|
|
||||||
float evaluate(Goals::TSubgoal task);
|
float evaluate(Goals::TSubgoal task, int priorityTier = BUILDINGS);
|
||||||
|
|
||||||
|
enum PriorityTier : int32_t
|
||||||
|
{
|
||||||
|
BUILDINGS = 0,
|
||||||
|
INSTAKILL,
|
||||||
|
INSTADEFEND,
|
||||||
|
KILL,
|
||||||
|
UPGRADE,
|
||||||
|
HIGH_PRIO_EXPLORE,
|
||||||
|
HUNTER_GATHER,
|
||||||
|
LOW_PRIO_EXPLORE,
|
||||||
|
DEFEND
|
||||||
|
};
|
||||||
|
|
||||||
private:
|
private:
|
||||||
const Nullkiller * ai;
|
const Nullkiller * ai;
|
||||||
|
@ -11,6 +11,8 @@
|
|||||||
#include <limits>
|
#include <limits>
|
||||||
|
|
||||||
#include "Settings.h"
|
#include "Settings.h"
|
||||||
|
|
||||||
|
#include "../../../lib/constants/StringConstants.h"
|
||||||
#include "../../../lib/mapObjectConstructors/AObjectTypeHandler.h"
|
#include "../../../lib/mapObjectConstructors/AObjectTypeHandler.h"
|
||||||
#include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h"
|
#include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h"
|
||||||
#include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
|
#include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
|
||||||
@ -22,56 +24,42 @@
|
|||||||
|
|
||||||
namespace NKAI
|
namespace NKAI
|
||||||
{
|
{
|
||||||
Settings::Settings()
|
Settings::Settings(int difficultyLevel)
|
||||||
: maxRoamingHeroes(8),
|
: maxRoamingHeroes(8),
|
||||||
mainHeroTurnDistanceLimit(10),
|
mainHeroTurnDistanceLimit(10),
|
||||||
scoutHeroTurnDistanceLimit(5),
|
scoutHeroTurnDistanceLimit(5),
|
||||||
maxGoldPressure(0.3f),
|
maxGoldPressure(0.3f),
|
||||||
|
retreatThresholdRelative(0.3),
|
||||||
|
retreatThresholdAbsolute(10000),
|
||||||
|
safeAttackRatio(1.1),
|
||||||
maxpass(10),
|
maxpass(10),
|
||||||
|
pathfinderBucketsCount(1),
|
||||||
|
pathfinderBucketSize(32),
|
||||||
allowObjectGraph(true),
|
allowObjectGraph(true),
|
||||||
useTroopsFromGarrisons(false),
|
useTroopsFromGarrisons(false),
|
||||||
openMap(true)
|
updateHitmapOnTileReveal(false),
|
||||||
|
openMap(true),
|
||||||
|
useFuzzy(false)
|
||||||
{
|
{
|
||||||
JsonNode node = JsonUtils::assembleFromFiles("config/ai/nkai/nkai-settings");
|
const std::string & difficultyName = GameConstants::DIFFICULTY_NAMES[difficultyLevel];
|
||||||
|
const JsonNode & rootNode = JsonUtils::assembleFromFiles("config/ai/nkai/nkai-settings");
|
||||||
|
const JsonNode & node = rootNode[difficultyName];
|
||||||
|
|
||||||
if(node.Struct()["maxRoamingHeroes"].isNumber())
|
maxRoamingHeroes = node["maxRoamingHeroes"].Integer();
|
||||||
{
|
mainHeroTurnDistanceLimit = node["mainHeroTurnDistanceLimit"].Integer();
|
||||||
maxRoamingHeroes = node.Struct()["maxRoamingHeroes"].Integer();
|
scoutHeroTurnDistanceLimit = node["scoutHeroTurnDistanceLimit"].Integer();
|
||||||
}
|
maxpass = node["maxpass"].Integer();
|
||||||
|
pathfinderBucketsCount = node["pathfinderBucketsCount"].Integer();
|
||||||
if(node.Struct()["mainHeroTurnDistanceLimit"].isNumber())
|
pathfinderBucketSize = node["pathfinderBucketSize"].Integer();
|
||||||
{
|
maxGoldPressure = node["maxGoldPressure"].Float();
|
||||||
mainHeroTurnDistanceLimit = node.Struct()["mainHeroTurnDistanceLimit"].Integer();
|
retreatThresholdRelative = node["retreatThresholdRelative"].Float();
|
||||||
}
|
retreatThresholdAbsolute = node["retreatThresholdAbsolute"].Float();
|
||||||
|
maxArmyLossTarget = node["maxArmyLossTarget"].Float();
|
||||||
if(node.Struct()["scoutHeroTurnDistanceLimit"].isNumber())
|
safeAttackRatio = node["safeAttackRatio"].Float();
|
||||||
{
|
allowObjectGraph = node["allowObjectGraph"].Bool();
|
||||||
scoutHeroTurnDistanceLimit = node.Struct()["scoutHeroTurnDistanceLimit"].Integer();
|
updateHitmapOnTileReveal = node["updateHitmapOnTileReveal"].Bool();
|
||||||
}
|
openMap = node["openMap"].Bool();
|
||||||
|
useFuzzy = node["useFuzzy"].Bool();
|
||||||
if(node.Struct()["maxpass"].isNumber())
|
useTroopsFromGarrisons = node["useTroopsFromGarrisons"].Bool();
|
||||||
{
|
|
||||||
maxpass = node.Struct()["maxpass"].Integer();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(node.Struct()["maxGoldPressure"].isNumber())
|
|
||||||
{
|
|
||||||
maxGoldPressure = node.Struct()["maxGoldPressure"].Float();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!node.Struct()["allowObjectGraph"].isNull())
|
|
||||||
{
|
|
||||||
allowObjectGraph = node.Struct()["allowObjectGraph"].Bool();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!node.Struct()["openMap"].isNull())
|
|
||||||
{
|
|
||||||
openMap = node.Struct()["openMap"].Bool();
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!node.Struct()["useTroopsFromGarrisons"].isNull())
|
|
||||||
{
|
|
||||||
useTroopsFromGarrisons = node.Struct()["useTroopsFromGarrisons"].Bool();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -25,21 +25,37 @@ namespace NKAI
|
|||||||
int mainHeroTurnDistanceLimit;
|
int mainHeroTurnDistanceLimit;
|
||||||
int scoutHeroTurnDistanceLimit;
|
int scoutHeroTurnDistanceLimit;
|
||||||
int maxpass;
|
int maxpass;
|
||||||
|
int pathfinderBucketsCount;
|
||||||
|
int pathfinderBucketSize;
|
||||||
float maxGoldPressure;
|
float maxGoldPressure;
|
||||||
|
float retreatThresholdRelative;
|
||||||
|
float retreatThresholdAbsolute;
|
||||||
|
float safeAttackRatio;
|
||||||
|
float maxArmyLossTarget;
|
||||||
bool allowObjectGraph;
|
bool allowObjectGraph;
|
||||||
bool useTroopsFromGarrisons;
|
bool useTroopsFromGarrisons;
|
||||||
|
bool updateHitmapOnTileReveal;
|
||||||
bool openMap;
|
bool openMap;
|
||||||
|
bool useFuzzy;
|
||||||
|
|
||||||
public:
|
public:
|
||||||
Settings();
|
explicit Settings(int difficultyLevel);
|
||||||
|
|
||||||
int getMaxPass() const { return maxpass; }
|
int getMaxPass() const { return maxpass; }
|
||||||
float getMaxGoldPressure() const { return maxGoldPressure; }
|
float getMaxGoldPressure() const { return maxGoldPressure; }
|
||||||
|
float getRetreatThresholdRelative() const { return retreatThresholdRelative; }
|
||||||
|
float getRetreatThresholdAbsolute() const { return retreatThresholdAbsolute; }
|
||||||
|
float getSafeAttackRatio() const { return safeAttackRatio; }
|
||||||
|
float getMaxArmyLossTarget() const { return maxArmyLossTarget; }
|
||||||
int getMaxRoamingHeroes() const { return maxRoamingHeroes; }
|
int getMaxRoamingHeroes() const { return maxRoamingHeroes; }
|
||||||
int getMainHeroTurnDistanceLimit() const { return mainHeroTurnDistanceLimit; }
|
int getMainHeroTurnDistanceLimit() const { return mainHeroTurnDistanceLimit; }
|
||||||
int getScoutHeroTurnDistanceLimit() const { return scoutHeroTurnDistanceLimit; }
|
int getScoutHeroTurnDistanceLimit() const { return scoutHeroTurnDistanceLimit; }
|
||||||
|
int getPathfinderBucketsCount() const { return pathfinderBucketsCount; }
|
||||||
|
int getPathfinderBucketSize() const { return pathfinderBucketSize; }
|
||||||
bool isObjectGraphAllowed() const { return allowObjectGraph; }
|
bool isObjectGraphAllowed() const { return allowObjectGraph; }
|
||||||
bool isGarrisonTroopsUsageAllowed() const { return useTroopsFromGarrisons; }
|
bool isGarrisonTroopsUsageAllowed() const { return useTroopsFromGarrisons; }
|
||||||
|
bool isUpdateHitmapOnTileReveal() const { return updateHitmapOnTileReveal; }
|
||||||
bool isOpenMap() const { return openMap; }
|
bool isOpenMap() const { return openMap; }
|
||||||
|
bool isUseFuzzy() const { return useFuzzy; }
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -104,6 +104,7 @@ namespace Goals
|
|||||||
bool isAbstract; SETTER(bool, isAbstract)
|
bool isAbstract; SETTER(bool, isAbstract)
|
||||||
int value; SETTER(int, value)
|
int value; SETTER(int, value)
|
||||||
ui64 goldCost; SETTER(ui64, goldCost)
|
ui64 goldCost; SETTER(ui64, goldCost)
|
||||||
|
TResources buildingCost; SETTER(TResources, buildingCost)
|
||||||
int resID; SETTER(int, resID)
|
int resID; SETTER(int, resID)
|
||||||
int objid; SETTER(int, objid)
|
int objid; SETTER(int, objid)
|
||||||
int aid; SETTER(int, aid)
|
int aid; SETTER(int, aid)
|
||||||
|
@ -53,6 +53,9 @@ void AdventureSpellCast::accept(AIGateway * ai)
|
|||||||
throw cannotFulfillGoalException("The town is already occupied by " + town->visitingHero->getNameTranslated());
|
throw cannotFulfillGoalException("The town is already occupied by " + town->visitingHero->getNameTranslated());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (hero->inTownGarrison)
|
||||||
|
ai->myCb->swapGarrisonHero(hero->visitedTown);
|
||||||
|
|
||||||
auto wait = cb->waitTillRealize;
|
auto wait = cb->waitTillRealize;
|
||||||
|
|
||||||
cb->waitTillRealize = true;
|
cb->waitTillRealize = true;
|
||||||
|
@ -90,9 +90,12 @@ void ExchangeSwapTownHeroes::accept(AIGateway * ai)
|
|||||||
|
|
||||||
if(!town->garrisonHero)
|
if(!town->garrisonHero)
|
||||||
{
|
{
|
||||||
while(upperArmy->stacksCount() != 0)
|
if (!garrisonHero->canBeMergedWith(*town))
|
||||||
{
|
{
|
||||||
cb->dismissCreature(upperArmy, upperArmy->Slots().begin()->first);
|
while (upperArmy->stacksCount() != 0)
|
||||||
|
{
|
||||||
|
cb->dismissCreature(upperArmy, upperArmy->Slots().begin()->first);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -22,6 +22,7 @@ ExecuteHeroChain::ExecuteHeroChain(const AIPath & path, const CGObjectInstance *
|
|||||||
{
|
{
|
||||||
hero = path.targetHero;
|
hero = path.targetHero;
|
||||||
tile = path.targetTile();
|
tile = path.targetTile();
|
||||||
|
closestWayRatio = 1;
|
||||||
|
|
||||||
if(obj)
|
if(obj)
|
||||||
{
|
{
|
||||||
@ -85,6 +86,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
|
|||||||
|
|
||||||
ai->nullkiller->setActive(chainPath.targetHero, tile);
|
ai->nullkiller->setActive(chainPath.targetHero, tile);
|
||||||
ai->nullkiller->setTargetObject(objid);
|
ai->nullkiller->setTargetObject(objid);
|
||||||
|
ai->nullkiller->objectClusterizer->reset();
|
||||||
|
|
||||||
auto targetObject = ai->myCb->getObj(static_cast<ObjectInstanceID>(objid), false);
|
auto targetObject = ai->myCb->getObj(static_cast<ObjectInstanceID>(objid), false);
|
||||||
|
|
||||||
|
@ -73,6 +73,7 @@ void RecruitHero::accept(AIGateway * ai)
|
|||||||
std::unique_lock lockGuard(ai->nullkiller->aiStateMutex);
|
std::unique_lock lockGuard(ai->nullkiller->aiStateMutex);
|
||||||
|
|
||||||
ai->nullkiller->heroManager->update();
|
ai->nullkiller->heroManager->update();
|
||||||
|
ai->nullkiller->objectClusterizer->reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -44,6 +44,7 @@ namespace Goals
|
|||||||
}
|
}
|
||||||
|
|
||||||
std::string toString() const override;
|
std::string toString() const override;
|
||||||
|
const CGHeroInstance* getHero() const override { return heroToBuy; }
|
||||||
void accept(AIGateway * ai) override;
|
void accept(AIGateway * ai) override;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
@ -36,16 +36,12 @@ std::string StayAtTown::toString() const
|
|||||||
{
|
{
|
||||||
return "Stay at town " + town->getNameTranslated()
|
return "Stay at town " + town->getNameTranslated()
|
||||||
+ " hero " + hero->getNameTranslated()
|
+ " hero " + hero->getNameTranslated()
|
||||||
+ ", mana: " + std::to_string(hero->mana);
|
+ ", mana: " + std::to_string(hero->mana)
|
||||||
|
+ " / " + std::to_string(hero->manaLimit());
|
||||||
}
|
}
|
||||||
|
|
||||||
void StayAtTown::accept(AIGateway * ai)
|
void StayAtTown::accept(AIGateway * ai)
|
||||||
{
|
{
|
||||||
if(hero->visitedTown != town)
|
|
||||||
{
|
|
||||||
logAi->error("Hero %s expected visiting town %s", hero->getNameTranslated(), town->getNameTranslated());
|
|
||||||
}
|
|
||||||
|
|
||||||
ai->nullkiller->lockHero(hero, HeroLockedReason::DEFENCE);
|
ai->nullkiller->lockHero(hero, HeroLockedReason::DEFENCE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -175,7 +175,7 @@ void ExplorationHelper::scanTile(const int3 & tile)
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if(isSafeToVisit(hero, path.heroArmy, path.getTotalDanger()))
|
if(isSafeToVisit(hero, path.heroArmy, path.getTotalDanger(), ai->settings->getSafeAttackRatio()))
|
||||||
{
|
{
|
||||||
bestGoal = goal;
|
bestGoal = goal;
|
||||||
bestValue = ourValue;
|
bestValue = ourValue;
|
||||||
|
@ -39,17 +39,17 @@ const uint64_t CHAIN_MAX_DEPTH = 4;
|
|||||||
|
|
||||||
const bool DO_NOT_SAVE_TO_COMMITTED_TILES = false;
|
const bool DO_NOT_SAVE_TO_COMMITTED_TILES = false;
|
||||||
|
|
||||||
AISharedStorage::AISharedStorage(int3 sizes)
|
AISharedStorage::AISharedStorage(int3 sizes, int numChains)
|
||||||
{
|
{
|
||||||
if(!shared){
|
if(!shared){
|
||||||
shared.reset(new boost::multi_array<AIPathNode, 4>(
|
shared.reset(new boost::multi_array<AIPathNode, 4>(
|
||||||
boost::extents[sizes.z][sizes.x][sizes.y][AIPathfinding::NUM_CHAINS]));
|
boost::extents[sizes.z][sizes.x][sizes.y][numChains]));
|
||||||
|
|
||||||
nodes = shared;
|
nodes = shared;
|
||||||
|
|
||||||
foreach_tile_pos([&](const int3 & pos)
|
foreach_tile_pos([&](const int3 & pos)
|
||||||
{
|
{
|
||||||
for(auto i = 0; i < AIPathfinding::NUM_CHAINS; i++)
|
for(auto i = 0; i < numChains; i++)
|
||||||
{
|
{
|
||||||
auto & node = get(pos)[i];
|
auto & node = get(pos)[i];
|
||||||
|
|
||||||
@ -92,8 +92,18 @@ void AIPathNode::addSpecialAction(std::shared_ptr<const SpecialAction> action)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
int AINodeStorage::getBucketCount() const
|
||||||
|
{
|
||||||
|
return ai->settings->getPathfinderBucketsCount();
|
||||||
|
}
|
||||||
|
|
||||||
|
int AINodeStorage::getBucketSize() const
|
||||||
|
{
|
||||||
|
return ai->settings->getPathfinderBucketSize();
|
||||||
|
}
|
||||||
|
|
||||||
AINodeStorage::AINodeStorage(const Nullkiller * ai, const int3 & Sizes)
|
AINodeStorage::AINodeStorage(const Nullkiller * ai, const int3 & Sizes)
|
||||||
: sizes(Sizes), ai(ai), cb(ai->cb.get()), nodes(Sizes)
|
: sizes(Sizes), ai(ai), cb(ai->cb.get()), nodes(Sizes, ai->settings->getPathfinderBucketSize() * ai->settings->getPathfinderBucketsCount())
|
||||||
{
|
{
|
||||||
accessibility = std::make_unique<boost::multi_array<EPathAccessibility, 4>>(
|
accessibility = std::make_unique<boost::multi_array<EPathAccessibility, 4>>(
|
||||||
boost::extents[sizes.z][sizes.x][sizes.y][EPathfindingLayer::NUM_LAYERS]);
|
boost::extents[sizes.z][sizes.x][sizes.y][EPathfindingLayer::NUM_LAYERS]);
|
||||||
@ -169,8 +179,8 @@ std::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
|
|||||||
const EPathfindingLayer layer,
|
const EPathfindingLayer layer,
|
||||||
const ChainActor * actor)
|
const ChainActor * actor)
|
||||||
{
|
{
|
||||||
int bucketIndex = ((uintptr_t)actor + static_cast<uint32_t>(layer)) % AIPathfinding::BUCKET_COUNT;
|
int bucketIndex = ((uintptr_t)actor + static_cast<uint32_t>(layer)) % ai->settings->getPathfinderBucketsCount();
|
||||||
int bucketOffset = bucketIndex * AIPathfinding::BUCKET_SIZE;
|
int bucketOffset = bucketIndex * ai->settings->getPathfinderBucketSize();
|
||||||
auto chains = nodes.get(pos);
|
auto chains = nodes.get(pos);
|
||||||
|
|
||||||
if(blocked(pos, layer))
|
if(blocked(pos, layer))
|
||||||
@ -178,7 +188,7 @@ std::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
|
|||||||
return std::nullopt;
|
return std::nullopt;
|
||||||
}
|
}
|
||||||
|
|
||||||
for(auto i = AIPathfinding::BUCKET_SIZE - 1; i >= 0; i--)
|
for(auto i = ai->settings->getPathfinderBucketSize() - 1; i >= 0; i--)
|
||||||
{
|
{
|
||||||
AIPathNode & node = chains[i + bucketOffset];
|
AIPathNode & node = chains[i + bucketOffset];
|
||||||
|
|
||||||
@ -486,8 +496,8 @@ public:
|
|||||||
AINodeStorage & storage, const std::vector<int3> & tiles, uint64_t chainMask, int heroChainTurn)
|
AINodeStorage & storage, const std::vector<int3> & tiles, uint64_t chainMask, int heroChainTurn)
|
||||||
:existingChains(), newChains(), delayedWork(), storage(storage), chainMask(chainMask), heroChainTurn(heroChainTurn), heroChain(), tiles(tiles)
|
:existingChains(), newChains(), delayedWork(), storage(storage), chainMask(chainMask), heroChainTurn(heroChainTurn), heroChain(), tiles(tiles)
|
||||||
{
|
{
|
||||||
existingChains.reserve(AIPathfinding::NUM_CHAINS);
|
existingChains.reserve(storage.getBucketCount() * storage.getBucketSize());
|
||||||
newChains.reserve(AIPathfinding::NUM_CHAINS);
|
newChains.reserve(storage.getBucketCount() * storage.getBucketSize());
|
||||||
}
|
}
|
||||||
|
|
||||||
void execute(const tbb::blocked_range<size_t>& r)
|
void execute(const tbb::blocked_range<size_t>& r)
|
||||||
@ -719,6 +729,7 @@ void HeroChainCalculationTask::calculateHeroChain(
|
|||||||
if(node->action == EPathNodeAction::BATTLE
|
if(node->action == EPathNodeAction::BATTLE
|
||||||
|| node->action == EPathNodeAction::TELEPORT_BATTLE
|
|| node->action == EPathNodeAction::TELEPORT_BATTLE
|
||||||
|| node->action == EPathNodeAction::TELEPORT_NORMAL
|
|| node->action == EPathNodeAction::TELEPORT_NORMAL
|
||||||
|
|| node->action == EPathNodeAction::DISEMBARK
|
||||||
|| node->action == EPathNodeAction::TELEPORT_BLOCKING_VISIT)
|
|| node->action == EPathNodeAction::TELEPORT_BLOCKING_VISIT)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
@ -961,7 +972,7 @@ void AINodeStorage::setHeroes(std::map<const CGHeroInstance *, HeroRole> heroes)
|
|||||||
// do not allow our own heroes in garrison to act on map
|
// do not allow our own heroes in garrison to act on map
|
||||||
if(hero.first->getOwner() == ai->playerID
|
if(hero.first->getOwner() == ai->playerID
|
||||||
&& hero.first->inTownGarrison
|
&& hero.first->inTownGarrison
|
||||||
&& (ai->isHeroLocked(hero.first) || ai->heroManager->heroCapReached()))
|
&& (ai->isHeroLocked(hero.first) || ai->heroManager->heroCapReached(false)))
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@ -1196,6 +1207,11 @@ void AINodeStorage::calculateTownPortal(
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (targetTown->visitingHero
|
||||||
|
&& (targetTown->visitingHero.get()->getFactionID() != actor->hero->getFactionID()
|
||||||
|
|| targetTown->getUpperArmy()->stacksCount()))
|
||||||
|
continue;
|
||||||
|
|
||||||
auto nodeOptional = townPortalFinder.createTownPortalNode(targetTown);
|
auto nodeOptional = townPortalFinder.createTownPortalNode(targetTown);
|
||||||
|
|
||||||
if(nodeOptional)
|
if(nodeOptional)
|
||||||
@ -1418,6 +1434,10 @@ void AINodeStorage::calculateChainInfo(std::vector<AIPath> & paths, const int3 &
|
|||||||
path.heroArmy = node.actor->creatureSet;
|
path.heroArmy = node.actor->creatureSet;
|
||||||
path.armyLoss = node.armyLoss;
|
path.armyLoss = node.armyLoss;
|
||||||
path.targetObjectDanger = ai->dangerEvaluator->evaluateDanger(pos, path.targetHero, !node.actor->allowBattle);
|
path.targetObjectDanger = ai->dangerEvaluator->evaluateDanger(pos, path.targetHero, !node.actor->allowBattle);
|
||||||
|
for (auto pathNode : path.nodes)
|
||||||
|
{
|
||||||
|
path.targetObjectDanger = std::max(ai->dangerEvaluator->evaluateDanger(pathNode.coord, path.targetHero, !node.actor->allowBattle), path.targetObjectDanger);
|
||||||
|
}
|
||||||
|
|
||||||
if(path.targetObjectDanger > 0)
|
if(path.targetObjectDanger > 0)
|
||||||
{
|
{
|
||||||
@ -1564,7 +1584,7 @@ uint8_t AIPath::turn() const
|
|||||||
|
|
||||||
uint64_t AIPath::getHeroStrength() const
|
uint64_t AIPath::getHeroStrength() const
|
||||||
{
|
{
|
||||||
return targetHero->getFightingStrength() * getHeroArmyStrengthWithCommander(targetHero, heroArmy);
|
return targetHero->getHeroStrength() * getHeroArmyStrengthWithCommander(targetHero, heroArmy);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint64_t AIPath::getTotalDanger() const
|
uint64_t AIPath::getTotalDanger() const
|
||||||
|
@ -29,9 +29,6 @@ namespace NKAI
|
|||||||
{
|
{
|
||||||
namespace AIPathfinding
|
namespace AIPathfinding
|
||||||
{
|
{
|
||||||
const int BUCKET_COUNT = 3;
|
|
||||||
const int BUCKET_SIZE = 7;
|
|
||||||
const int NUM_CHAINS = BUCKET_COUNT * BUCKET_SIZE;
|
|
||||||
const int CHAIN_MAX_DEPTH = 4;
|
const int CHAIN_MAX_DEPTH = 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,7 +154,7 @@ public:
|
|||||||
static boost::mutex locker;
|
static boost::mutex locker;
|
||||||
static uint32_t version;
|
static uint32_t version;
|
||||||
|
|
||||||
AISharedStorage(int3 mapSize);
|
AISharedStorage(int3 sizes, int numChains);
|
||||||
~AISharedStorage();
|
~AISharedStorage();
|
||||||
|
|
||||||
STRONG_INLINE
|
STRONG_INLINE
|
||||||
@ -197,6 +194,9 @@ public:
|
|||||||
bool selectFirstActor();
|
bool selectFirstActor();
|
||||||
bool selectNextActor();
|
bool selectNextActor();
|
||||||
|
|
||||||
|
int getBucketCount() const;
|
||||||
|
int getBucketSize() const;
|
||||||
|
|
||||||
std::vector<CGPathNode *> getInitialNodes() override;
|
std::vector<CGPathNode *> getInitialNodes() override;
|
||||||
|
|
||||||
virtual void calculateNeighbours(
|
virtual void calculateNeighbours(
|
||||||
@ -298,7 +298,7 @@ public:
|
|||||||
|
|
||||||
inline int getBucket(const ChainActor * actor) const
|
inline int getBucket(const ChainActor * actor) const
|
||||||
{
|
{
|
||||||
return ((uintptr_t)actor * 395) % AIPathfinding::BUCKET_COUNT;
|
return ((uintptr_t)actor * 395) % getBucketCount();
|
||||||
}
|
}
|
||||||
|
|
||||||
void calculateTownPortalTeleportations(std::vector<CGPathNode *> & neighbours);
|
void calculateTownPortalTeleportations(std::vector<CGPathNode *> & neighbours);
|
||||||
|
@ -46,7 +46,7 @@ ChainActor::ChainActor(const CGHeroInstance * hero, HeroRole heroRole, uint64_t
|
|||||||
initialMovement = hero->movementPointsRemaining();
|
initialMovement = hero->movementPointsRemaining();
|
||||||
initialTurn = 0;
|
initialTurn = 0;
|
||||||
armyValue = getHeroArmyStrengthWithCommander(hero, hero);
|
armyValue = getHeroArmyStrengthWithCommander(hero, hero);
|
||||||
heroFightingStrength = hero->getFightingStrength();
|
heroFightingStrength = hero->getHeroStrength();
|
||||||
tiCache.reset(new TurnInfo(hero));
|
tiCache.reset(new TurnInfo(hero));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -25,11 +25,9 @@ using crstring = const std::string &;
|
|||||||
using dwellingContent = std::pair<ui32, std::vector<CreatureID>>;
|
using dwellingContent = std::pair<ui32, std::vector<CreatureID>>;
|
||||||
|
|
||||||
const int ACTUAL_RESOURCE_COUNT = 7;
|
const int ACTUAL_RESOURCE_COUNT = 7;
|
||||||
const int ALLOWED_ROAMING_HEROES = 8;
|
|
||||||
|
|
||||||
//implementation-dependent
|
//implementation-dependent
|
||||||
extern const double SAFE_ATTACK_CONSTANT;
|
extern const double SAFE_ATTACK_CONSTANT;
|
||||||
extern const int GOLD_RESERVE;
|
|
||||||
|
|
||||||
extern thread_local CCallback * cb;
|
extern thread_local CCallback * cb;
|
||||||
extern thread_local VCAI * ai;
|
extern thread_local VCAI * ai;
|
||||||
|
@ -14,8 +14,6 @@
|
|||||||
#include "../../CCallback.h"
|
#include "../../CCallback.h"
|
||||||
#include "../../lib/mapObjects/MapObjects.h"
|
#include "../../lib/mapObjects/MapObjects.h"
|
||||||
|
|
||||||
#define GOLD_RESERVE (10000); //at least we'll be able to reach capitol
|
|
||||||
|
|
||||||
ResourceObjective::ResourceObjective(const TResources & Res, Goals::TSubgoal Goal)
|
ResourceObjective::ResourceObjective(const TResources & Res, Goals::TSubgoal Goal)
|
||||||
: resources(Res), goal(Goal)
|
: resources(Res), goal(Goal)
|
||||||
{
|
{
|
||||||
|
@ -1314,8 +1314,6 @@ bool VCAI::canRecruitAnyHero(const CGTownInstance * t) const
|
|||||||
return false;
|
return false;
|
||||||
if(cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST) //TODO: use ResourceManager
|
if(cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST) //TODO: use ResourceManager
|
||||||
return false;
|
return false;
|
||||||
if(cb->getHeroesInfo().size() >= ALLOWED_ROAMING_HEROES)
|
|
||||||
return false;
|
|
||||||
if(cb->getHeroesInfo().size() >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP))
|
if(cb->getHeroesInfo().size() >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP))
|
||||||
return false;
|
return false;
|
||||||
if(!cb->getAvailableHeroes(t).size())
|
if(!cb->getAvailableHeroes(t).size())
|
||||||
|
278
CI/example.markdownlint-cli2.jsonc
Normal file
@ -0,0 +1,278 @@
|
|||||||
|
{
|
||||||
|
"config" : {
|
||||||
|
"default" : true,
|
||||||
|
|
||||||
|
// MD001/heading-increment : Heading levels should only increment by one level at a time : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md001.md
|
||||||
|
"MD001": false,
|
||||||
|
|
||||||
|
// MD003/heading-style : Heading style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md003.md
|
||||||
|
"MD003": {
|
||||||
|
"style": "atx"
|
||||||
|
},
|
||||||
|
|
||||||
|
// MD004/ul-style : Unordered list style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md004.md
|
||||||
|
"MD004": false,
|
||||||
|
// FIXME: enable and consider fixing
|
||||||
|
//{
|
||||||
|
// "style": "consistent"
|
||||||
|
//},
|
||||||
|
|
||||||
|
// MD005/list-indent : Inconsistent indentation for list items at the same level : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md005.md
|
||||||
|
"MD005": true,
|
||||||
|
|
||||||
|
// MD007/ul-indent : Unordered list indentation : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md007.md
|
||||||
|
"MD007": {
|
||||||
|
// Spaces for indent
|
||||||
|
"indent": 2,
|
||||||
|
// Whether to indent the first level of the list
|
||||||
|
"start_indented": false,
|
||||||
|
// Spaces for first level indent (when start_indented is set)
|
||||||
|
"start_indent": 0
|
||||||
|
},
|
||||||
|
|
||||||
|
// MD009/no-trailing-spaces : Trailing spaces : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md009.md
|
||||||
|
"MD009": {
|
||||||
|
// Spaces for line break
|
||||||
|
"br_spaces": 2,
|
||||||
|
// Allow spaces for empty lines in list items
|
||||||
|
"list_item_empty_lines": false,
|
||||||
|
// Include unnecessary breaks
|
||||||
|
"strict": false
|
||||||
|
},
|
||||||
|
|
||||||
|
// MD010/no-hard-tabs : Hard tabs : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md010.md
|
||||||
|
"MD010": {
|
||||||
|
// Include code blocks
|
||||||
|
"code_blocks": false,
|
||||||
|
// Fenced code languages to ignore
|
||||||
|
"ignore_code_languages": [],
|
||||||
|
// Number of spaces for each hard tab
|
||||||
|
"spaces_per_tab": 4
|
||||||
|
},
|
||||||
|
|
||||||
|
// MD011/no-reversed-links : Reversed link syntax : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md011.md
|
||||||
|
"MD011": true,
|
||||||
|
|
||||||
|
// MD012/no-multiple-blanks : Multiple consecutive blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md012.md
|
||||||
|
"MD012": {
|
||||||
|
// Consecutive blank lines
|
||||||
|
"maximum": 1
|
||||||
|
},
|
||||||
|
|
||||||
|
// MD013/line-length : Line length : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md013.md
|
||||||
|
"MD013": false,
|
||||||
|
|
||||||
|
// MD014/commands-show-output : Dollar signs used before commands without showing output : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md014.md
|
||||||
|
"MD014": true,
|
||||||
|
|
||||||
|
// MD018/no-missing-space-atx : No space after hash on atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md018.md
|
||||||
|
"MD018": true,
|
||||||
|
|
||||||
|
// MD019/no-multiple-space-atx : Multiple spaces after hash on atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md019.md
|
||||||
|
"MD019": true,
|
||||||
|
|
||||||
|
// MD020/no-missing-space-closed-atx : No space inside hashes on closed atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md020.md
|
||||||
|
"MD020": true,
|
||||||
|
|
||||||
|
// MD021/no-multiple-space-closed-atx : Multiple spaces inside hashes on closed atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md021.md
|
||||||
|
"MD021": true,
|
||||||
|
|
||||||
|
// MD022/blanks-around-headings : Headings should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md022.md
|
||||||
|
"MD022": {
|
||||||
|
// Blank lines above heading
|
||||||
|
"lines_above": 1,
|
||||||
|
// Blank lines below heading
|
||||||
|
"lines_below": 1
|
||||||
|
},
|
||||||
|
|
||||||
|
// MD023/heading-start-left : Headings must start at the beginning of the line : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md023.md
|
||||||
|
"MD023": true,
|
||||||
|
|
||||||
|
// MD024/no-duplicate-heading : Multiple headings with the same content : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md024.md
|
||||||
|
"MD024": false,
|
||||||
|
// FIXME: false positives?
|
||||||
|
//{
|
||||||
|
// // Only check sibling headings
|
||||||
|
// "allow_different_nesting": true,
|
||||||
|
// // Only check sibling headings
|
||||||
|
// "siblings_only": true
|
||||||
|
//},
|
||||||
|
|
||||||
|
// MD025/single-title/single-h1 : Multiple top-level headings in the same document : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md025.md
|
||||||
|
"MD025": {
|
||||||
|
// Heading level
|
||||||
|
"level": 1,
|
||||||
|
// RegExp for matching title in front matter
|
||||||
|
"front_matter_title": "^\\s*title\\s*[:=]"
|
||||||
|
},
|
||||||
|
|
||||||
|
// MD026/no-trailing-punctuation : Trailing punctuation in heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md026.md
|
||||||
|
"MD026": {
|
||||||
|
// Punctuation characters
|
||||||
|
"punctuation": ".,;:!。,;:!"
|
||||||
|
},
|
||||||
|
|
||||||
|
// MD027/no-multiple-space-blockquote : Multiple spaces after blockquote symbol : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md027.md
|
||||||
|
"MD027": true,
|
||||||
|
|
||||||
|
// MD028/no-blanks-blockquote : Blank line inside blockquote : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md028.md
|
||||||
|
"MD028": true,
|
||||||
|
|
||||||
|
// MD029/ol-prefix : Ordered list item prefix : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md029.md
|
||||||
|
"MD029": false,
|
||||||
|
// FIXME: false positives or broken formatting
|
||||||
|
//{
|
||||||
|
// // List style
|
||||||
|
// "style": "ordered"
|
||||||
|
//},
|
||||||
|
|
||||||
|
// MD030/list-marker-space : Spaces after list markers : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md030.md
|
||||||
|
"MD030": {
|
||||||
|
// Spaces for single-line unordered list items
|
||||||
|
"ul_single": 1,
|
||||||
|
// Spaces for single-line ordered list items
|
||||||
|
"ol_single": 1,
|
||||||
|
// Spaces for multi-line unordered list items
|
||||||
|
"ul_multi": 1,
|
||||||
|
// Spaces for multi-line ordered list items
|
||||||
|
"ol_multi": 1
|
||||||
|
},
|
||||||
|
|
||||||
|
// MD031/blanks-around-fences : Fenced code blocks should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md031.md
|
||||||
|
"MD031": {
|
||||||
|
// Include list items
|
||||||
|
"list_items": false
|
||||||
|
},
|
||||||
|
|
||||||
|
// MD032/blanks-around-lists : Lists should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md032.md
|
||||||
|
"MD032": true,
|
||||||
|
|
||||||
|
// MD033/no-inline-html : Inline HTML : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md033.md
|
||||||
|
"MD033": false,
|
||||||
|
// FIXME: enable and consider fixing
|
||||||
|
//{
|
||||||
|
// // Allowed elements
|
||||||
|
// "allowed_elements": []
|
||||||
|
//},
|
||||||
|
|
||||||
|
// MD034/no-bare-urls : Bare URL used : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md034.md
|
||||||
|
"MD034": true,
|
||||||
|
|
||||||
|
// MD035/hr-style : Horizontal rule style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md035.md
|
||||||
|
"MD035": {
|
||||||
|
// Horizontal rule style
|
||||||
|
"style": "consistent"
|
||||||
|
},
|
||||||
|
|
||||||
|
// MD036/no-emphasis-as-heading : Emphasis used instead of a heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md036.md
|
||||||
|
"MD036": false,
|
||||||
|
// FIXME: enable and consider fixing
|
||||||
|
// {
|
||||||
|
// // Punctuation characters
|
||||||
|
// "punctuation": ".,;:!?。,;:!?"
|
||||||
|
// },
|
||||||
|
|
||||||
|
// MD037/no-space-in-emphasis : Spaces inside emphasis markers : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md037.md
|
||||||
|
"MD037": true,
|
||||||
|
|
||||||
|
// MD038/no-space-in-code : Spaces inside code span elements : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md038.md
|
||||||
|
"MD038": true,
|
||||||
|
|
||||||
|
// MD039/no-space-in-links : Spaces inside link text : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md039.md
|
||||||
|
"MD039": true,
|
||||||
|
|
||||||
|
// MD040/fenced-code-language : Fenced code blocks should have a language specified : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md040.md
|
||||||
|
"MD040": {
|
||||||
|
// List of languages
|
||||||
|
"allowed_languages": [ "cpp", "json", "sh", "text", "nix", "powershell", "lua" ],
|
||||||
|
// Require language only
|
||||||
|
"language_only": true
|
||||||
|
},
|
||||||
|
|
||||||
|
// MD041/first-line-heading/first-line-h1 : First line in a file should be a top-level heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md041.md
|
||||||
|
"MD041": {
|
||||||
|
// Heading level
|
||||||
|
"level": 1,
|
||||||
|
// RegExp for matching title in front matter
|
||||||
|
"front_matter_title": "^\\s*title\\s*[:=]"
|
||||||
|
},
|
||||||
|
|
||||||
|
// MD042/no-empty-links : No empty links : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md042.md
|
||||||
|
"MD042": true,
|
||||||
|
|
||||||
|
// MD043/required-headings : Required heading structure : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md043.md
|
||||||
|
"MD043": false,
|
||||||
|
|
||||||
|
// MD044/proper-names : Proper names should have the correct capitalization : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md044.md
|
||||||
|
"MD044": false,
|
||||||
|
|
||||||
|
// MD045/no-alt-text : Images should have alternate text (alt text) : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md045.md
|
||||||
|
"MD045": false,
|
||||||
|
|
||||||
|
// MD046/code-block-style : Code block style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md046.md
|
||||||
|
"MD046": {
|
||||||
|
// Block style
|
||||||
|
"style": "fenced"
|
||||||
|
},
|
||||||
|
|
||||||
|
// MD047/single-trailing-newline : Files should end with a single newline character : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md047.md
|
||||||
|
"MD047": true,
|
||||||
|
|
||||||
|
// MD048/code-fence-style : Code fence style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md048.md
|
||||||
|
"MD048": {
|
||||||
|
// Code fence style
|
||||||
|
"style": "backtick"
|
||||||
|
},
|
||||||
|
|
||||||
|
// MD049/emphasis-style : Emphasis style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md049.md
|
||||||
|
"MD049": {
|
||||||
|
// Emphasis style
|
||||||
|
"style": "asterisk"
|
||||||
|
},
|
||||||
|
|
||||||
|
// MD050/strong-style : Strong style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md050.md
|
||||||
|
"MD050": {
|
||||||
|
// Strong style
|
||||||
|
"style": "asterisk"
|
||||||
|
},
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// MD051/link-fragments : Link fragments should be valid : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md051.md
|
||||||
|
"MD051": true,
|
||||||
|
|
||||||
|
// MD052/reference-links-images : Reference links and images should use a label that is defined : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md052.md
|
||||||
|
"MD052": {
|
||||||
|
// Include shortcut syntax
|
||||||
|
"shortcut_syntax": false
|
||||||
|
},
|
||||||
|
|
||||||
|
// MD053/link-image-reference-definitions : Link and image reference definitions should be needed : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md053.md
|
||||||
|
"MD053": {
|
||||||
|
// Ignored definitions
|
||||||
|
"ignored_definitions": [
|
||||||
|
"//"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
// MD054/link-image-style : Link and image style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md054.md
|
||||||
|
"MD054": {
|
||||||
|
// Allow autolinks
|
||||||
|
"autolink": true,
|
||||||
|
// Allow inline links and images
|
||||||
|
"inline": true,
|
||||||
|
// Allow full reference links and images
|
||||||
|
"full": true,
|
||||||
|
// Allow collapsed reference links and images
|
||||||
|
"collapsed": true,
|
||||||
|
// Allow shortcut reference links and images
|
||||||
|
"shortcut": true,
|
||||||
|
// Allow URLs as inline links
|
||||||
|
"url_inline": true
|
||||||
|
},
|
||||||
|
|
||||||
|
// MD058 - Tables should be surrounded by blank lines
|
||||||
|
"MD058" : true
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
1506
ChangeLog.md
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 422 B After Width: | Height: | Size: 422 B |
Before Width: | Height: | Size: 272 B After Width: | Height: | Size: 272 B |
BIN
Mods/vcmi/Content/Sprites/battle/queueDefend.png
Normal file
After Width: | Height: | Size: 594 B |
BIN
Mods/vcmi/Content/Sprites/battle/queueWait.png
Normal file
After Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 139 B After Width: | Height: | Size: 139 B |
Before Width: | Height: | Size: 457 B After Width: | Height: | Size: 457 B |
Before Width: | Height: | Size: 184 B After Width: | Height: | Size: 184 B |
Before Width: | Height: | Size: 307 B After Width: | Height: | Size: 307 B |
Before Width: | Height: | Size: 245 B After Width: | Height: | Size: 245 B |
Before Width: | Height: | Size: 222 B After Width: | Height: | Size: 222 B |
Before Width: | Height: | Size: 377 B After Width: | Height: | Size: 377 B |
Before Width: | Height: | Size: 347 B After Width: | Height: | Size: 347 B |
Before Width: | Height: | Size: 139 B After Width: | Height: | Size: 139 B |
Before Width: | Height: | Size: 455 B After Width: | Height: | Size: 455 B |
Before Width: | Height: | Size: 181 B After Width: | Height: | Size: 181 B |
Before Width: | Height: | Size: 305 B After Width: | Height: | Size: 305 B |
Before Width: | Height: | Size: 238 B After Width: | Height: | Size: 238 B |
Before Width: | Height: | Size: 217 B After Width: | Height: | Size: 217 B |
Before Width: | Height: | Size: 370 B After Width: | Height: | Size: 370 B |
Before Width: | Height: | Size: 343 B After Width: | Height: | Size: 343 B |
8
Mods/vcmi/Content/Sprites/cprsmall.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"images" :
|
||||||
|
[
|
||||||
|
// Fix for swapped in H3 icons of Wight and Wraith
|
||||||
|
{ "frame" : 62, "defFile" : "cprsmall.def", "defFrame" : 63},
|
||||||
|
{ "frame" : 63, "defFile" : "cprsmall.def", "defFrame" : 62}
|
||||||
|
]
|
||||||
|
}
|
Before Width: | Height: | Size: 111 B After Width: | Height: | Size: 111 B |
Before Width: | Height: | Size: 102 B After Width: | Height: | Size: 102 B |
Before Width: | Height: | Size: 96 B After Width: | Height: | Size: 96 B |
Before Width: | Height: | Size: 112 B After Width: | Height: | Size: 112 B |
Before Width: | Height: | Size: 133 B After Width: | Height: | Size: 133 B |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 489 B After Width: | Height: | Size: 489 B |
Before Width: | Height: | Size: 487 B After Width: | Height: | Size: 487 B |
Before Width: | Height: | Size: 401 B After Width: | Height: | Size: 401 B |
Before Width: | Height: | Size: 522 B After Width: | Height: | Size: 522 B |
Before Width: | Height: | Size: 374 B After Width: | Height: | Size: 374 B |
Before Width: | Height: | Size: 997 B After Width: | Height: | Size: 997 B |
Before Width: | Height: | Size: 986 B After Width: | Height: | Size: 986 B |