1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-07-13 01:20:34 +02:00

Merge branch 'beta' into map_editor_open_recent-v2

This commit is contained in:
Ivan Savenko
2024-12-09 16:51:07 +02:00
committed by GitHub
611 changed files with 11785 additions and 7117 deletions

View File

@ -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 '....'
@ -24,7 +25,7 @@ Steps to reproduce the behavior:
A clear and concise description of what you expected to happen. A clear and concise description of what you expected to happen.
**Actual behavior** **Actual behavior**
A clear description what is currently happening A clear description what is currently happening
**Did it work earlier?** **Did it work earlier?**
If this something which worked well some time ago, please let us know about version where it works or at date when it worked. If this something which worked well some time ago, please let us know about version where it works or at date when it worked.
@ -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.

View File

@ -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'

View File

@ -675,7 +675,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
spells::BattleCast cast(state.get(), hero, spells::Mode::HERO, ps.spell); spells::BattleCast cast(state.get(), hero, spells::Mode::HERO, ps.spell);
cast.castEval(state->getServerCallback(), ps.dest); cast.castEval(state->getServerCallback(), ps.dest);
auto allUnits = state->battleGetUnitsIf([](const battle::Unit * u) -> bool { return true; }); auto allUnits = state->battleGetUnitsIf([](const battle::Unit * u) -> bool { return u->isValidTarget(); });
auto needFullEval = vstd::contains_if(allUnits, [&](const battle::Unit * u) -> bool auto needFullEval = vstd::contains_if(allUnits, [&](const battle::Unit * u) -> bool
{ {
@ -731,7 +731,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
} }
} }

View File

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

View File

@ -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;
} }

View File

@ -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)

View File

@ -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);

View File

@ -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();

View File

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

View File

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

View File

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

View File

@ -95,7 +95,7 @@ float HeroManager::evaluateSpeciality(const CGHeroInstance * hero) const
float HeroManager::evaluateFightingStrength(const CGHeroInstance * hero) const float HeroManager::evaluateFightingStrength(const CGHeroInstance * hero) const
{ {
return evaluateSpeciality(hero) + wariorSkillsScores.evaluateSecSkills(hero) + hero->level * 1.5f; return evaluateSpeciality(hero) + wariorSkillsScores.evaluateSecSkills(hero) + hero->getBasePrimarySkillValue(PrimarySkill::ATTACK) + hero->getBasePrimarySkillValue(PrimarySkill::DEFENSE) + hero->getBasePrimarySkillValue(PrimarySkill::SPELL_POWER) + hero->getBasePrimarySkillValue(PrimarySkill::KNOWLEDGE);
} }
void HeroManager::update() void HeroManager::update()
@ -108,7 +108,7 @@ void HeroManager::update()
for(auto & hero : myHeroes) for(auto & hero : myHeroes)
{ {
scores[hero] = evaluateFightingStrength(hero); scores[hero] = evaluateFightingStrength(hero);
knownFightingStrength[hero->id] = hero->getFightingStrength(); knownFightingStrength[hero->id] = hero->getHeroStrength();
} }
auto scoreSort = [&](const CGHeroInstance * h1, const CGHeroInstance * h2) -> bool auto scoreSort = [&](const CGHeroInstance * h1, const CGHeroInstance * h2) -> bool
@ -147,7 +147,10 @@ void HeroManager::update()
HeroRole HeroManager::getHeroRole(const HeroPtr & hero) const HeroRole HeroManager::getHeroRole(const HeroPtr & hero) const
{ {
return heroRoles.at(hero); if (heroRoles.find(hero) != heroRoles.end())
return heroRoles.at(hero);
else
return HeroRole::SCOUT;
} }
const std::map<HeroPtr, HeroRole> & HeroManager::getHeroRoles() const const std::map<HeroPtr, HeroRole> & HeroManager::getHeroRoles() const
@ -188,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;
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -41,6 +41,9 @@ Goals::TGoalVec DefenceBehavior::decompose(const Nullkiller * ai) const
for(auto town : ai->cb->getTownsInfo()) for(auto town : ai->cb->getTownsInfo())
{ {
evaluateDefence(tasks, town, ai); evaluateDefence(tasks, town, ai);
//Let's do only one defence-task per pass since otherwise it can try to hire the same hero twice
if (!tasks.empty())
break;
} }
return tasks; return tasks;
@ -130,7 +133,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa
tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5))); tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5)));
return true; return false;
} }
else if(ai->heroManager->getHeroRole(town->garrisonHero.get()) == HeroRole::MAIN) else if(ai->heroManager->getHeroRole(town->garrisonHero.get()) == HeroRole::MAIN)
{ {
@ -141,7 +144,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa
{ {
tasks.push_back(Goals::sptr(Goals::DismissHero(heroToDismiss).setpriority(5))); tasks.push_back(Goals::sptr(Goals::DismissHero(heroToDismiss).setpriority(5)));
return true; return false;
} }
} }
} }
@ -158,11 +161,10 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
threats.push_back(threatNode.fastestDanger); // no guarantee that fastest danger will be there threats.push_back(threatNode.fastestDanger); // no guarantee that fastest danger will be there
if(town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks, ai)) if (town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks, ai))
{ {
return; return;
} }
if(!threatNode.fastestDanger.hero) if(!threatNode.fastestDanger.hero)
{ {
logAi->trace("No threat found for town %s", town->getNameTranslated()); logAi->trace("No threat found for town %s", town->getNameTranslated());
@ -250,6 +252,16 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
continue; continue;
} }
if (!path.targetHero->canBeMergedWith(*town))
{
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Can't merge armies of hero %s and town %s",
path.targetHero->getObjectName(),
town->getObjectName());
#endif
continue;
}
if(path.targetHero == town->visitingHero.get() && path.exchangeCount == 1) if(path.targetHero == town->visitingHero.get() && path.exchangeCount == 1)
{ {
#if NKAI_TRACE_LEVEL >= 1 #if NKAI_TRACE_LEVEL >= 1
@ -261,6 +273,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
// dismiss creatures we are not able to pick to be able to hide in garrison // dismiss creatures we are not able to pick to be able to hide in garrison
if(town->garrisonHero if(town->garrisonHero
|| town->getUpperArmy()->stacksCount() == 0 || town->getUpperArmy()->stacksCount() == 0
|| path.targetHero->canBeMergedWith(*town)
|| (town->getUpperArmy()->getArmyStrength() < 500 && town->fortLevel() >= CGTownInstance::CITADEL)) || (town->getUpperArmy()->getArmyStrength() < 500 && town->fortLevel() >= CGTownInstance::CITADEL))
{ {
tasks.push_back( tasks.push_back(
@ -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;

View File

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

View File

@ -81,6 +81,9 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con
logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength()); logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength());
#endif #endif
if (path.targetHero->getOwner() != ai->playerID)
continue;
if(path.containsHero(hero)) if(path.containsHero(hero))
{ {
#if NKAI_TRACE_LEVEL >= 2 #if NKAI_TRACE_LEVEL >= 2
@ -89,14 +92,6 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con
continue; continue;
} }
if(path.turn() > 0 && ai->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength());
#endif
continue;
}
if(ai->arePathHeroesLocked(path)) if(ai->arePathHeroesLocked(path))
{ {
#if NKAI_TRACE_LEVEL >= 2 #if NKAI_TRACE_LEVEL >= 2
@ -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(

View File

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

View File

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

View File

@ -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()
{ {

View File

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

View File

@ -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;
if(openMap && !canUseOpenMap(cb, playerID)) settings = std::make_unique<Settings>(cb->getStartInfo()->difficulty);
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;
}
} }

View File

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

View File

@ -15,6 +15,8 @@
#include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h" #include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h"
#include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h" #include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
#include "../../../lib/mapObjects/MapObjects.h" #include "../../../lib/mapObjects/MapObjects.h"
#include "../../../lib/mapping/CMapDefines.h"
#include "../../../lib/RoadHandler.h"
#include "../../../lib/CCreatureHandler.h" #include "../../../lib/CCreatureHandler.h"
#include "../../../lib/VCMI_Lib.h" #include "../../../lib/VCMI_Lib.h"
#include "../../../lib/StartInfo.h" #include "../../../lib/StartInfo.h"
@ -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);

View File

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

View File

@ -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();
}
} }
} }

View File

@ -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; }
}; };
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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;

View File

@ -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

View File

@ -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);

View File

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

View File

@ -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;

View File

@ -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)
{ {

View File

@ -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())

View 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
}
}

File diff suppressed because it is too large Load Diff

View File

Before

Width:  |  Height:  |  Size: 5.5 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 2.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

Before

Width:  |  Height:  |  Size: 2.0 KiB

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

Before

Width:  |  Height:  |  Size: 422 B

After

Width:  |  Height:  |  Size: 422 B

View File

Before

Width:  |  Height:  |  Size: 272 B

After

Width:  |  Height:  |  Size: 272 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 594 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.6 KiB

View File

Before

Width:  |  Height:  |  Size: 139 B

After

Width:  |  Height:  |  Size: 139 B

View File

Before

Width:  |  Height:  |  Size: 457 B

After

Width:  |  Height:  |  Size: 457 B

View File

Before

Width:  |  Height:  |  Size: 184 B

After

Width:  |  Height:  |  Size: 184 B

View File

Before

Width:  |  Height:  |  Size: 307 B

After

Width:  |  Height:  |  Size: 307 B

View File

Before

Width:  |  Height:  |  Size: 245 B

After

Width:  |  Height:  |  Size: 245 B

View File

Before

Width:  |  Height:  |  Size: 222 B

After

Width:  |  Height:  |  Size: 222 B

View File

Before

Width:  |  Height:  |  Size: 139 B

After

Width:  |  Height:  |  Size: 139 B

View File

Before

Width:  |  Height:  |  Size: 455 B

After

Width:  |  Height:  |  Size: 455 B

View File

Before

Width:  |  Height:  |  Size: 181 B

After

Width:  |  Height:  |  Size: 181 B

View File

Before

Width:  |  Height:  |  Size: 305 B

After

Width:  |  Height:  |  Size: 305 B

View File

Before

Width:  |  Height:  |  Size: 238 B

After

Width:  |  Height:  |  Size: 238 B

View File

Before

Width:  |  Height:  |  Size: 217 B

After

Width:  |  Height:  |  Size: 217 B

View File

Before

Width:  |  Height:  |  Size: 370 B

After

Width:  |  Height:  |  Size: 370 B

View 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}
]
}

View File

Before

Width:  |  Height:  |  Size: 111 B

After

Width:  |  Height:  |  Size: 111 B

View File

Before

Width:  |  Height:  |  Size: 102 B

After

Width:  |  Height:  |  Size: 102 B

View File

Before

Width:  |  Height:  |  Size: 96 B

After

Width:  |  Height:  |  Size: 96 B

View File

Before

Width:  |  Height:  |  Size: 112 B

After

Width:  |  Height:  |  Size: 112 B

View File

Before

Width:  |  Height:  |  Size: 133 B

After

Width:  |  Height:  |  Size: 133 B

View File

Before

Width:  |  Height:  |  Size: 2.8 KiB

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

Before

Width:  |  Height:  |  Size: 489 B

After

Width:  |  Height:  |  Size: 489 B

View File

Before

Width:  |  Height:  |  Size: 487 B

After

Width:  |  Height:  |  Size: 487 B

View File

Before

Width:  |  Height:  |  Size: 401 B

After

Width:  |  Height:  |  Size: 401 B

View File

Before

Width:  |  Height:  |  Size: 522 B

After

Width:  |  Height:  |  Size: 522 B

View File

Before

Width:  |  Height:  |  Size: 374 B

After

Width:  |  Height:  |  Size: 374 B

View File

Before

Width:  |  Height:  |  Size: 997 B

After

Width:  |  Height:  |  Size: 997 B

View File

Before

Width:  |  Height:  |  Size: 986 B

After

Width:  |  Height:  |  Size: 986 B

Some files were not shown because too many files have changed in this diff Show More