Merge branch 'beta' into map_editor_open_recent-v2
8
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@ -15,6 +15,7 @@ Please attach game logs: `VCMI_client.txt`, `VCMI_server.txt` etc.
|
||||
|
||||
**To Reproduce**
|
||||
Steps to reproduce the behavior:
|
||||
|
||||
1. Go to '...'
|
||||
2. Click on '....'
|
||||
3. Scroll down to '....'
|
||||
@ -24,7 +25,7 @@ Steps to reproduce the behavior:
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
**Actual behavior**
|
||||
A clear description what is currently happening
|
||||
A clear description what is currently happening
|
||||
|
||||
**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.
|
||||
@ -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.
|
||||
|
||||
**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**
|
||||
Add any other context about the problem here.
|
||||
|
6
.github/workflows/github.yml
vendored
@ -402,3 +402,9 @@ jobs:
|
||||
run: |
|
||||
sudo apt install python3-jstyleson
|
||||
python3 CI/validate_json.py
|
||||
|
||||
- name: Validate Markdown
|
||||
uses: DavidAnson/markdownlint-cli2-action@v18
|
||||
with:
|
||||
config: 'CI/example.markdownlint-cli2.jsonc'
|
||||
globs: '**/*.md'
|
||||
|
@ -675,7 +675,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
|
||||
spells::BattleCast cast(state.get(), hero, spells::Mode::HERO, ps.spell);
|
||||
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
|
||||
{
|
||||
@ -731,7 +731,6 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
|
||||
|
||||
ps.value = scoreEvaluator.evaluateExchange(updatedAttack, cachedAttack.turn, *targets, innerCache, state);
|
||||
}
|
||||
|
||||
for(const auto & unit : allUnits)
|
||||
{
|
||||
if(!unit->isValidTarget(true))
|
||||
@ -771,11 +770,31 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
|
||||
ps.value -= 4 * dpsReduce * scoreEvaluator.getNegativeEffectMultiplier();
|
||||
|
||||
#if BATTLE_TRACE_LEVEL >= 1
|
||||
logAi->trace(
|
||||
"Spell affects %s (%d), dps: %2f",
|
||||
unit->creatureId().toCreature()->getNameSingularTranslated(),
|
||||
unit->getCount(),
|
||||
dpsReduce);
|
||||
// Ensure ps.dest is not empty before accessing the first element
|
||||
if (!ps.dest.empty())
|
||||
{
|
||||
logAi->trace(
|
||||
"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
|
||||
}
|
||||
}
|
||||
|
@ -906,7 +906,7 @@ std::vector<const battle::Unit *> BattleExchangeEvaluator::getOneTurnReachableUn
|
||||
{
|
||||
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];
|
||||
HypotheticBattle turnBattle(env.get(), cb);
|
||||
|
@ -34,11 +34,6 @@
|
||||
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
|
||||
thread_local CCallback * cb = 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))
|
||||
addVisitableObj(obj);
|
||||
}
|
||||
|
||||
if (nullkiller->settings->isUpdateHitmapOnTileReveal())
|
||||
nullkiller->dangerHitMap->reset();
|
||||
}
|
||||
|
||||
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();
|
||||
|
||||
// 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);
|
||||
}
|
||||
@ -670,7 +668,7 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector<C
|
||||
else if(objType == Obj::ARTIFACT || objType == Obj::RESOURCE)
|
||||
{
|
||||
bool dangerUnknown = danger == 0;
|
||||
bool dangerTooHigh = ratio > (1 / SAFE_ATTACK_CONSTANT);
|
||||
bool dangerTooHigh = ratio * nullkiller->settings->getSafeAttackRatio() > 1;
|
||||
|
||||
answer = !dangerUnknown && !dangerTooHigh;
|
||||
}
|
||||
|
@ -146,21 +146,21 @@ bool HeroPtr::operator==(const HeroPtr & rhs) const
|
||||
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)
|
||||
{
|
||||
return heroStrength / SAFE_ATTACK_CONSTANT > dangerStrength;
|
||||
return heroStrength > dangerStrength * safeAttackRatio;
|
||||
}
|
||||
|
||||
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)
|
||||
|
@ -61,11 +61,6 @@ const int GOLD_MINE_PRODUCTION = 1000;
|
||||
const int WOOD_ORE_MINE_PRODUCTION = 2;
|
||||
const int RESOURCE_MINE_PRODUCTION = 1;
|
||||
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;
|
||||
|
||||
@ -213,8 +208,8 @@ bool isBlockVisitObj(const int3 & pos);
|
||||
bool isWeeklyRevisitable(const Nullkiller * ai, const CGObjectInstance * obj);
|
||||
|
||||
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, const CCreatureSet *, uint64_t dangerStrength);
|
||||
bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength, float safeAttackRatio);
|
||||
bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet *, uint64_t dangerStrength, float safeAttackRatio);
|
||||
|
||||
bool compareHeroStrength(const CGHeroInstance * h1, const CGHeroInstance * h2);
|
||||
bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2);
|
||||
|
@ -13,6 +13,7 @@
|
||||
#include "../Engine/Nullkiller.h"
|
||||
#include "../../../CCallback.h"
|
||||
#include "../../../lib/mapObjects/MapObjects.h"
|
||||
#include "../../../lib/IGameSettings.h"
|
||||
#include "../../../lib/GameConstants.h"
|
||||
|
||||
namespace NKAI
|
||||
@ -152,16 +153,6 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
|
||||
uint64_t armyValue = 0;
|
||||
|
||||
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())
|
||||
{
|
||||
@ -197,16 +188,18 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
|
||||
auto morale = slot.second->moraleVal();
|
||||
auto multiplier = 1.0f;
|
||||
|
||||
const float BadMoraleChance = 0.083f;
|
||||
const float HighMoraleChance = 0.04f;
|
||||
const auto & badMoraleDice = cb->getSettings().getVector(EGameSettings::COMBAT_BAD_MORALE_DICE);
|
||||
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();
|
||||
|
@ -39,7 +39,6 @@ void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo)
|
||||
for(int upgradeIndex : {1, 0})
|
||||
{
|
||||
BuildingID building = BuildingID(BuildingID::getDwellingFromLevel(level, upgradeIndex));
|
||||
|
||||
if(!vstd::contains(buildings, building))
|
||||
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)
|
||||
{
|
||||
otherBuildings.push_back({BuildingID::CITADEL, BuildingID::CASTLE});
|
||||
otherBuildings.push_back({BuildingID::HORDE_1});
|
||||
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 & buildingID : buildingSet)
|
||||
@ -141,6 +147,8 @@ void BuildAnalyzer::update()
|
||||
|
||||
auto towns = ai->cb->getTownsInfo();
|
||||
|
||||
float economyDevelopmentCost = 0;
|
||||
|
||||
for(const CGTownInstance* town : towns)
|
||||
{
|
||||
logAi->trace("Checking town %s", town->getNameTranslated());
|
||||
@ -153,6 +161,11 @@ void BuildAnalyzer::update()
|
||||
|
||||
requiredResources += developmentInfo.requiredResources;
|
||||
totalDevelopmentCost += developmentInfo.townDevelopmentCost;
|
||||
for(auto building : developmentInfo.toBuild)
|
||||
{
|
||||
if (building.dailyIncome[EGameResID::GOLD] > 0)
|
||||
economyDevelopmentCost += building.buildCostWithPrerequisites[EGameResID::GOLD];
|
||||
}
|
||||
armyCost += developmentInfo.armyCost;
|
||||
|
||||
for(auto bi : developmentInfo.toBuild)
|
||||
@ -171,15 +184,7 @@ void BuildAnalyzer::update()
|
||||
|
||||
updateDailyIncome();
|
||||
|
||||
if(ai->cb->getDate(Date::DAY) == 1)
|
||||
{
|
||||
goldPressure = 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
goldPressure = ai->getLockedResources()[EGameResID::GOLD] / 5000.0f
|
||||
+ (float)armyCost[EGameResID::GOLD] / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f);
|
||||
}
|
||||
goldPressure = (ai->getLockedResources()[EGameResID::GOLD] + (float)armyCost[EGameResID::GOLD] + economyDevelopmentCost) / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f);
|
||||
|
||||
logAi->trace("Gold pressure: %f", goldPressure);
|
||||
}
|
||||
@ -237,6 +242,12 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
|
||||
logAi->trace("checking %s", info.name);
|
||||
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))
|
||||
{
|
||||
auto canBuild = ai->cb->canBuildStructure(town, building);
|
||||
@ -281,7 +292,15 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
|
||||
prerequisite.baseCreatureID = info.baseCreatureID;
|
||||
prerequisite.prerequisitesCount++;
|
||||
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;
|
||||
}
|
||||
|
@ -89,7 +89,6 @@ void DangerHitMapAnalyzer::updateHitMap()
|
||||
|
||||
heroes[hero->tempOwner][hero] = HeroRole::MAIN;
|
||||
}
|
||||
|
||||
if(obj->ID == Obj::TOWN)
|
||||
{
|
||||
auto town = dynamic_cast<const CGTownInstance *>(obj);
|
||||
@ -140,6 +139,7 @@ void DangerHitMapAnalyzer::updateHitMap()
|
||||
|
||||
newThreat.hero = path.targetHero;
|
||||
newThreat.turn = path.turn();
|
||||
newThreat.threat = path.getHeroStrength() * (1 - path.movementCost() / 2.0);
|
||||
newThreat.danger = path.getHeroStrength();
|
||||
|
||||
if(newThreat.value() > node.maximumDanger.value())
|
||||
@ -316,8 +316,8 @@ uint64_t DangerHitMapAnalyzer::enemyCanKillOurHeroesAlongThePath(const AIPath &
|
||||
|
||||
const auto& info = getTileThreat(tile);
|
||||
|
||||
return (info.fastestDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.fastestDanger.danger))
|
||||
|| (info.maximumDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.maximumDanger.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, ai->settings->getSafeAttackRatio()));
|
||||
}
|
||||
|
||||
const HitMapNode & DangerHitMapAnalyzer::getObjectThreat(const CGObjectInstance * obj) const
|
||||
@ -348,6 +348,7 @@ std::set<const CGObjectInstance *> DangerHitMapAnalyzer::getOneTurnAccessibleObj
|
||||
void DangerHitMapAnalyzer::reset()
|
||||
{
|
||||
hitMapUpToDate = false;
|
||||
tileOwnersUpToDate = false;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -22,6 +22,7 @@ struct HitMapInfo
|
||||
|
||||
uint64_t danger;
|
||||
uint8_t turn;
|
||||
float threat;
|
||||
HeroPtr hero;
|
||||
|
||||
HitMapInfo()
|
||||
@ -33,6 +34,7 @@ struct HitMapInfo
|
||||
{
|
||||
danger = 0;
|
||||
turn = 255;
|
||||
threat = 0;
|
||||
hero = HeroPtr();
|
||||
}
|
||||
|
||||
|
@ -95,7 +95,7 @@ float HeroManager::evaluateSpeciality(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()
|
||||
@ -108,7 +108,7 @@ void HeroManager::update()
|
||||
for(auto & hero : myHeroes)
|
||||
{
|
||||
scores[hero] = evaluateFightingStrength(hero);
|
||||
knownFightingStrength[hero->id] = hero->getFightingStrength();
|
||||
knownFightingStrength[hero->id] = hero->getHeroStrength();
|
||||
}
|
||||
|
||||
auto scoreSort = [&](const CGHeroInstance * h1, const CGHeroInstance * h2) -> bool
|
||||
@ -147,7 +147,10 @@ void HeroManager::update()
|
||||
|
||||
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
|
||||
@ -188,13 +191,11 @@ float HeroManager::evaluateHero(const CGHeroInstance * hero) const
|
||||
return evaluateFightingStrength(hero);
|
||||
}
|
||||
|
||||
bool HeroManager::heroCapReached() const
|
||||
bool HeroManager::heroCapReached(bool includeGarrisoned) const
|
||||
{
|
||||
const bool includeGarnisoned = true;
|
||||
int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned);
|
||||
int heroCount = cb->getHeroCount(ai->playerID, includeGarrisoned);
|
||||
|
||||
return heroCount >= ALLOWED_ROAMING_HEROES
|
||||
|| heroCount >= ai->settings->getMaxRoamingHeroes()
|
||||
return heroCount >= ai->settings->getMaxRoamingHeroes()
|
||||
|| heroCount >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_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);
|
||||
|
||||
//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
|
||||
@ -281,7 +282,7 @@ const CGHeroInstance * HeroManager::findHeroWithGrail() const
|
||||
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;
|
||||
auto myHeroes = ai->cb->getHeroesInfo();
|
||||
@ -292,12 +293,13 @@ const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) co
|
||||
|| existingHero->getArmyStrength() >armyLimit
|
||||
|| getHeroRole(existingHero) == HeroRole::MAIN
|
||||
|| existingHero->movementPointsRemaining()
|
||||
|| (townToSpare != nullptr && existingHero->visitedTown == townToSpare)
|
||||
|| existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength())
|
||||
if(!weakestHero || weakestHero->getHeroStrength() > existingHero->getHeroStrength())
|
||||
{
|
||||
weakestHero = existingHero;
|
||||
}
|
||||
|
@ -56,9 +56,9 @@ public:
|
||||
float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const;
|
||||
float evaluateHero(const CGHeroInstance * hero) const;
|
||||
bool canRecruitHero(const CGTownInstance * t = nullptr) const;
|
||||
bool heroCapReached() const;
|
||||
bool heroCapReached(bool includeGarrisoned = true) 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 getFightingStrengthCached(const CGHeroInstance * hero) const;
|
||||
|
||||
|
@ -97,9 +97,10 @@ std::optional<const CGObjectInstance *> ObjectClusterizer::getBlocker(const AIPa
|
||||
{
|
||||
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));
|
||||
|
||||
@ -474,9 +475,11 @@ void ObjectClusterizer::clusterizeObject(
|
||||
|
||||
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;
|
||||
|
||||
ClusterMap::accessor cluster;
|
||||
@ -495,9 +498,11 @@ void ObjectClusterizer::clusterizeObject(
|
||||
|
||||
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;
|
||||
|
||||
bool interestingObject = path.turn() <= 2 || priority > 0.5f;
|
||||
|
@ -49,26 +49,49 @@ Goals::TGoalVec BuildingBehavior::decompose(const Nullkiller * ai) const
|
||||
auto & developmentInfos = ai->buildAnalyzer->getDevelopmentInfo();
|
||||
auto isGoldPressureLow = !ai->buildAnalyzer->isGoldPressureHigh();
|
||||
|
||||
ai->dangerHitMap->updateHitMap();
|
||||
|
||||
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)));
|
||||
emergencyDefense = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!emergencyDefense)
|
||||
{
|
||||
for (auto& buildingInfo : developmentInfo.toBuild)
|
||||
{
|
||||
if (isGoldPressureLow || buildingInfo.dailyIncome[EGameResID::GOLD] > 0)
|
||||
{
|
||||
if (buildingInfo.notEnoughRes)
|
||||
{
|
||||
if (ai->getLockedResources().canAfford(buildingInfo.buildCost))
|
||||
continue;
|
||||
|
||||
Composition composition;
|
||||
|
||||
composition.addNext(BuildThis(buildingInfo, developmentInfo));
|
||||
composition.addNext(SaveResources(buildingInfo.buildCost));
|
||||
tasks.push_back(sptr(composition));
|
||||
}
|
||||
else
|
||||
{
|
||||
tasks.push_back(sptr(BuildThis(buildingInfo, developmentInfo)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -28,9 +28,6 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const
|
||||
{
|
||||
Goals::TGoalVec tasks;
|
||||
|
||||
if(ai->cb->getDate(Date::DAY) == 1)
|
||||
return tasks;
|
||||
|
||||
auto heroes = cb->getHeroesInfo();
|
||||
|
||||
if(heroes.empty())
|
||||
@ -38,19 +35,23 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const
|
||||
return tasks;
|
||||
}
|
||||
|
||||
ai->dangerHitMap->updateHitMap();
|
||||
|
||||
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(
|
||||
town,
|
||||
ai->getFreeResources());
|
||||
|
||||
for(const CGHeroInstance * targetHero : heroes)
|
||||
{
|
||||
if(ai->buildAnalyzer->isGoldPressureHigh() && !town->hasBuilt(BuildingID::CITY_HALL))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if(ai->heroManager->getHeroRole(targetHero) == HeroRole::MAIN)
|
||||
{
|
||||
auto reinforcement = ai->armyManager->howManyReinforcementsCanGet(
|
||||
|
@ -68,14 +68,6 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(
|
||||
logAi->trace("Path found %s", path.toString());
|
||||
#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 NKAI_TRACE_LEVEL >= 2
|
||||
@ -87,6 +79,9 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(
|
||||
auto hero = path.targetHero;
|
||||
auto danger = path.getTotalDanger();
|
||||
|
||||
if (hero->getOwner() != nullkiller->playerID)
|
||||
continue;
|
||||
|
||||
if(nullkiller->heroManager->getHeroRole(hero) == HeroRole::SCOUT
|
||||
&& (path.getTotalDanger() == 0 || path.turn() > 0)
|
||||
&& path.exchangeCount > 1)
|
||||
@ -119,7 +114,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(
|
||||
continue;
|
||||
}
|
||||
|
||||
auto isSafe = isSafeToVisit(hero, path.heroArmy, danger);
|
||||
auto isSafe = isSafeToVisit(hero, path.heroArmy, danger, nullkiller->settings->getSafeAttackRatio());
|
||||
|
||||
#if NKAI_TRACE_LEVEL >= 2
|
||||
logAi->trace(
|
||||
|
@ -41,6 +41,9 @@ Goals::TGoalVec DefenceBehavior::decompose(const Nullkiller * ai) const
|
||||
for(auto town : ai->cb->getTownsInfo())
|
||||
{
|
||||
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;
|
||||
@ -130,7 +133,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa
|
||||
|
||||
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)
|
||||
{
|
||||
@ -141,7 +144,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa
|
||||
{
|
||||
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
|
||||
|
||||
if(town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks, ai))
|
||||
if (town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks, ai))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if(!threatNode.fastestDanger.hero)
|
||||
{
|
||||
logAi->trace("No threat found for town %s", town->getNameTranslated());
|
||||
@ -250,6 +252,16 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
|
||||
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 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
|
||||
if(town->garrisonHero
|
||||
|| town->getUpperArmy()->stacksCount() == 0
|
||||
|| path.targetHero->canBeMergedWith(*town)
|
||||
|| (town->getUpperArmy()->getArmyStrength() < 500 && town->fortLevel() >= CGTownInstance::CITADEL))
|
||||
{
|
||||
tasks.push_back(
|
||||
@ -292,7 +305,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
|
||||
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))
|
||||
{
|
||||
@ -343,23 +356,14 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
|
||||
}
|
||||
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
|
||||
logAi->trace("Cancel moving %s to defend town %s as the town has garrison hero",
|
||||
path.targetHero->getObjectName(),
|
||||
town->getObjectName());
|
||||
logAi->trace("Cancel moving %s to defend town %s as the town has garrison hero",
|
||||
path.targetHero->getObjectName(),
|
||||
town->getObjectName());
|
||||
#endif
|
||||
continue;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
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
|
||||
{
|
||||
if (threat.turn > 0 || town->garrisonHero || town->visitingHero)
|
||||
return;
|
||||
|
||||
if(town->hasBuilt(BuildingID::TAVERN)
|
||||
&& 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())
|
||||
{
|
||||
heroToDismiss = ai->heroManager->findWeakHeroToDismiss(hero->getArmyStrength());
|
||||
heroToDismiss = ai->heroManager->findWeakHeroToDismiss(hero->getArmyStrength(), town);
|
||||
|
||||
if(!heroToDismiss)
|
||||
continue;
|
||||
|
@ -33,48 +33,32 @@ Goals::TGoalVec ExplorationBehavior::decompose(const Nullkiller * ai) const
|
||||
{
|
||||
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::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;
|
||||
}
|
||||
case Obj::MONOLITH_ONE_WAY_ENTRANCE:
|
||||
case Obj::MONOLITH_TWO_WAY:
|
||||
case Obj::SUBTERRANEAN_GATE:
|
||||
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:
|
||||
case Obj::SUBTERRANEAN_GATE:
|
||||
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)
|
||||
auto tObj = dynamic_cast<const CGTeleport*>(obj);
|
||||
for (auto exit : cb->getTeleportChannelExits(tObj->channel))
|
||||
{
|
||||
if(!cb->getObj(exit))
|
||||
{
|
||||
// Always attempt to visit two-way teleports if one of channel exits is not visible
|
||||
tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj))));
|
||||
break;
|
||||
if (exit != tObj->id)
|
||||
{
|
||||
if (!cb->isVisible(cb->getObjInstance(exit)))
|
||||
tasks.push_back(sptr(Composition().addNext(ExplorationPoint(obj->visitablePos(), 50)).addNext(CaptureObject(obj))));
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -81,6 +81,9 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con
|
||||
logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength());
|
||||
#endif
|
||||
|
||||
if (path.targetHero->getOwner() != ai->playerID)
|
||||
continue;
|
||||
|
||||
if(path.containsHero(hero))
|
||||
{
|
||||
#if NKAI_TRACE_LEVEL >= 2
|
||||
@ -89,14 +92,6 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con
|
||||
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 NKAI_TRACE_LEVEL >= 2
|
||||
@ -150,7 +145,7 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con
|
||||
}
|
||||
|
||||
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
|
||||
logAi->trace(
|
||||
@ -292,17 +287,6 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
|
||||
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);
|
||||
|
||||
if(!upgrader->garrisonHero
|
||||
@ -320,14 +304,6 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
|
||||
|
||||
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.upgradeCost += armyToGetOrBuy.upgradeCost;
|
||||
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))
|
||||
{
|
||||
auto scoutReinforcement = ai->armyManager->howManyReinforcementsCanBuy(hero, upgrader)
|
||||
+ ai->armyManager->howManyReinforcementsCanGet(hero, upgrader);
|
||||
auto scoutReinforcement = ai->armyManager->howManyReinforcementsCanGet(hero, upgrader);
|
||||
|
||||
if(scoutReinforcement >= armyToGetOrBuy.upgradeValue
|
||||
&& ai->getFreeGold() >20000
|
||||
@ -366,7 +341,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
|
||||
|
||||
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
|
||||
logAi->trace(
|
||||
|
@ -31,9 +31,11 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
|
||||
|
||||
auto ourHeroes = ai->heroManager->getHeroRoles();
|
||||
auto minScoreToHireMain = std::numeric_limits<float>::max();
|
||||
int currentArmyValue = 0;
|
||||
|
||||
for(auto hero : ourHeroes)
|
||||
{
|
||||
currentArmyValue += hero.first->getArmyCost();
|
||||
if(hero.second != HeroRole::MAIN)
|
||||
continue;
|
||||
|
||||
@ -45,51 +47,88 @@ Goals::TGoalVec RecruitHeroBehavior::decompose(const Nullkiller * ai) const
|
||||
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)
|
||||
{
|
||||
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))
|
||||
{
|
||||
auto availableHeroes = ai->cb->getAvailableHeroes(town);
|
||||
|
||||
for(auto hero : availableHeroes)
|
||||
|
||||
for (auto obj : ai->objectClusterizer->getNearbyObjects())
|
||||
{
|
||||
auto score = ai->heroManager->evaluateHero(hero);
|
||||
|
||||
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)
|
||||
if ((obj->ID == Obj::RESOURCE)
|
||||
|| obj->ID == Obj::TREASURE_CHEST
|
||||
|| obj->ID == Obj::CAMPFIRE
|
||||
|| isWeeklyRevisitable(ai, obj)
|
||||
|| obj->ID ==Obj::ARTIFACT)
|
||||
|| obj->ID == Obj::ARTIFACT)
|
||||
{
|
||||
auto tile = obj->visitablePos();
|
||||
auto closestTown = ai->dangerHitMap->getClosestTown(tile);
|
||||
|
||||
if(town == closestTown)
|
||||
if (town == closestTown)
|
||||
treasureSourcesCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if(treasureSourcesCount < 5 && (town->garrisonHero || town->getUpperArmy()->getArmyStrength() < 10000))
|
||||
continue;
|
||||
|
||||
if(ai->cb->getHeroesInfo().size() < ai->cb->getTownsInfo().size() + 1
|
||||
|| (ai->getFreeResources()[EGameResID::GOLD] > 10000 && !ai->buildAnalyzer->isGoldPressureHigh()))
|
||||
for(auto hero : availableHeroes)
|
||||
{
|
||||
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;
|
||||
|
@ -39,9 +39,6 @@ Goals::TGoalVec StayAtTownBehavior::decompose(const Nullkiller * ai) const
|
||||
|
||||
for(auto town : towns)
|
||||
{
|
||||
if(!town->hasBuilt(BuildingID::MAGES_GUILD_1))
|
||||
continue;
|
||||
|
||||
ai->pathfinder->calculatePathInfo(paths, town->visitablePos());
|
||||
|
||||
for(auto & path : paths)
|
||||
@ -49,14 +46,8 @@ Goals::TGoalVec StayAtTownBehavior::decompose(const Nullkiller * ai) const
|
||||
if(town->visitingHero && town->visitingHero.get() != path.targetHero)
|
||||
continue;
|
||||
|
||||
if(!path.targetHero->hasSpellbook() || path.targetHero->mana >= 0.75f * path.targetHero->manaLimit())
|
||||
continue;
|
||||
|
||||
if(path.turn() == 0 && !path.getFirstBlockedAction() && path.exchangeCount <= 1)
|
||||
if(!path.getFirstBlockedAction() && path.exchangeCount <= 1)
|
||||
{
|
||||
if(path.targetHero->mana == path.targetHero->manaLimit())
|
||||
continue;
|
||||
|
||||
Composition stayAtTown;
|
||||
|
||||
stayAtTown.addNextSequence({
|
||||
|
@ -17,8 +17,7 @@
|
||||
namespace NKAI
|
||||
{
|
||||
|
||||
#define 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
|
||||
constexpr float MIN_AI_STRENGTH = 0.5f; //lower when combat AI gets smarter
|
||||
|
||||
engineBase::engineBase()
|
||||
{
|
||||
|
@ -52,6 +52,15 @@ ui64 FuzzyHelper::evaluateDanger(const int3 & tile, const CGHeroInstance * visit
|
||||
{
|
||||
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)
|
||||
@ -117,10 +126,10 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj)
|
||||
{
|
||||
auto fortLevel = town->fortLevel();
|
||||
|
||||
if(fortLevel == CGTownInstance::EFortLevel::CASTLE)
|
||||
danger += 10000;
|
||||
if (fortLevel == CGTownInstance::EFortLevel::CASTLE)
|
||||
danger = std::max(danger * 2, danger + 10000);
|
||||
else if(fortLevel == CGTownInstance::EFortLevel::CITADEL)
|
||||
danger += 4000;
|
||||
danger = std::max(ui64(danger * 1.4), danger + 4000);
|
||||
}
|
||||
|
||||
return danger;
|
||||
|
@ -34,13 +34,12 @@ using namespace Goals;
|
||||
std::unique_ptr<ObjectGraph> Nullkiller::baseGraph;
|
||||
|
||||
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)
|
||||
@ -62,17 +61,23 @@ bool canUseOpenMap(std::shared_ptr<CCallback> cb, PlayerColor playerID)
|
||||
return false;
|
||||
}
|
||||
|
||||
return cb->getStartInfo()->difficulty >= 3;
|
||||
return true;
|
||||
}
|
||||
|
||||
void Nullkiller::init(std::shared_ptr<CCallback> cb, AIGateway * gateway)
|
||||
{
|
||||
this->cb = cb;
|
||||
this->gateway = gateway;
|
||||
|
||||
playerID = gateway->playerID;
|
||||
this->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;
|
||||
openMap = false;
|
||||
@ -122,11 +127,14 @@ void TaskPlan::merge(TSubgoal task)
|
||||
{
|
||||
TGoalVec blockers;
|
||||
|
||||
if (task->asTask()->priority <= 0)
|
||||
return;
|
||||
|
||||
for(auto & item : tasks)
|
||||
{
|
||||
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)
|
||||
return;
|
||||
@ -166,20 +174,19 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TGoalVec & tasks) const
|
||||
return taskptr(*bestTask);
|
||||
}
|
||||
|
||||
Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks) const
|
||||
Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks, int priorityTier) const
|
||||
{
|
||||
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();
|
||||
|
||||
for(size_t i = r.begin(); i != r.end(); i++)
|
||||
{
|
||||
auto task = tasks[i];
|
||||
|
||||
if(task->asTask()->priority <= 0)
|
||||
task->asTask()->priority = evaluator->evaluate(task);
|
||||
if (task->asTask()->priority <= 0 || priorityTier != PriorityEvaluator::PriorityTier::BUILDINGS)
|
||||
task->asTask()->priority = evaluator->evaluate(task, priorityTier);
|
||||
}
|
||||
});
|
||||
|
||||
@ -326,7 +333,7 @@ bool Nullkiller::arePathHeroesLocked(const AIPath & path) const
|
||||
if(lockReason != HeroLockedReason::NOT_LOCKED)
|
||||
{
|
||||
#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
|
||||
return true;
|
||||
}
|
||||
@ -347,12 +354,24 @@ void Nullkiller::makeTurn()
|
||||
boost::lock_guard<boost::mutex> sharedStorageLock(AISharedStorage::locker);
|
||||
|
||||
const int MAX_DEPTH = 10;
|
||||
const float FAST_TASK_MINIMAL_PRIORITY = 0.7f;
|
||||
|
||||
resetAiState();
|
||||
|
||||
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++)
|
||||
{
|
||||
auto start = std::chrono::high_resolution_clock::now();
|
||||
@ -360,17 +379,21 @@ void Nullkiller::makeTurn()
|
||||
|
||||
Goals::TTask bestTask = taskptr(Goals::Invalid());
|
||||
|
||||
for(;i <= settings->getMaxPass(); i++)
|
||||
while(true)
|
||||
{
|
||||
bestTasks.clear();
|
||||
|
||||
decompose(bestTasks, sptr(RecruitHeroBehavior()), 1);
|
||||
decompose(bestTasks, sptr(BuyArmyBehavior()), 1);
|
||||
decompose(bestTasks, sptr(BuildingBehavior()), 1);
|
||||
|
||||
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))
|
||||
return;
|
||||
|
||||
@ -382,7 +405,6 @@ void Nullkiller::makeTurn()
|
||||
}
|
||||
}
|
||||
|
||||
decompose(bestTasks, sptr(RecruitHeroBehavior()), 1);
|
||||
decompose(bestTasks, sptr(CaptureObjectsBehavior()), 1);
|
||||
decompose(bestTasks, sptr(ClusterBehavior()), MAX_DEPTH);
|
||||
decompose(bestTasks, sptr(DefenceBehavior()), MAX_DEPTH);
|
||||
@ -392,12 +414,24 @@ void Nullkiller::makeTurn()
|
||||
if(!isOpenMap())
|
||||
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));
|
||||
|
||||
@ -438,7 +472,7 @@ void Nullkiller::makeTurn()
|
||||
bestTask->priority);
|
||||
}
|
||||
|
||||
if(bestTask->priority < MIN_PRIORITY)
|
||||
if((settings->isUseFuzzy() && bestTask->priority < MIN_PRIORITY) || (!settings->isUseFuzzy() && bestTask->priority <= 0))
|
||||
{
|
||||
auto heroes = cb->getHeroesInfo();
|
||||
auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool
|
||||
@ -463,7 +497,9 @@ void Nullkiller::makeTurn()
|
||||
|
||||
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(hasAnySuccess)
|
||||
@ -471,13 +507,27 @@ void Nullkiller::makeTurn()
|
||||
else
|
||||
return;
|
||||
}
|
||||
|
||||
hasAnySuccess = true;
|
||||
}
|
||||
|
||||
hasAnySuccess |= handleTrading();
|
||||
|
||||
if(!hasAnySuccess)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
@ -554,4 +604,102 @@ void Nullkiller::lockResources(const TResources & res)
|
||||
lockedResources += res;
|
||||
}
|
||||
|
||||
bool Nullkiller::handleTrading()
|
||||
{
|
||||
bool haveTraded = false;
|
||||
bool shouldTryToTrade = true;
|
||||
int marketId = -1;
|
||||
for (auto town : cb->getTownsInfo())
|
||||
{
|
||||
if (town->hasBuiltSomeTradeBuilding())
|
||||
{
|
||||
marketId = town->id;
|
||||
}
|
||||
}
|
||||
if (marketId == -1)
|
||||
return false;
|
||||
if (const CGObjectInstance* obj = cb->getObj(ObjectInstanceID(marketId), false))
|
||||
{
|
||||
if (const auto* m = dynamic_cast<const IMarket*>(obj))
|
||||
{
|
||||
while (shouldTryToTrade)
|
||||
{
|
||||
shouldTryToTrade = false;
|
||||
buildAnalyzer->update();
|
||||
TResources required = buildAnalyzer->getTotalResourcesRequired();
|
||||
TResources income = buildAnalyzer->getDailyIncome();
|
||||
TResources available = cb->getResourceAmount();
|
||||
#if NKAI_TRACE_LEVEL >= 2
|
||||
logAi->debug("Available %s", available.toString());
|
||||
logAi->debug("Required %s", required.toString());
|
||||
#endif
|
||||
int mostWanted = -1;
|
||||
int mostExpendable = -1;
|
||||
float minRatio = std::numeric_limits<float>::max();
|
||||
float maxRatio = std::numeric_limits<float>::min();
|
||||
|
||||
for (int i = 0; i < required.size(); ++i)
|
||||
{
|
||||
if (required[i] <= 0)
|
||||
continue;
|
||||
float ratio = static_cast<float>(available[i]) / required[i];
|
||||
|
||||
if (ratio < minRatio) {
|
||||
minRatio = ratio;
|
||||
mostWanted = i;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < required.size(); ++i)
|
||||
{
|
||||
float ratio = available[i];
|
||||
if (required[i] > 0)
|
||||
ratio = static_cast<float>(available[i]) / required[i];
|
||||
else
|
||||
ratio = available[i];
|
||||
|
||||
bool okToSell = false;
|
||||
|
||||
if (i == GameResID::GOLD)
|
||||
{
|
||||
if (income[i] > 0 && !buildAnalyzer->isGoldPressureHigh())
|
||||
okToSell = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (required[i] <= 0 && income[i] > 0)
|
||||
okToSell = true;
|
||||
}
|
||||
|
||||
if (ratio > maxRatio && okToSell) {
|
||||
maxRatio = ratio;
|
||||
mostExpendable = i;
|
||||
}
|
||||
}
|
||||
#if NKAI_TRACE_LEVEL >= 2
|
||||
logAi->debug("mostExpendable: %d mostWanted: %d", mostExpendable, mostWanted);
|
||||
#endif
|
||||
if (mostExpendable == mostWanted || mostWanted == -1 || mostExpendable == -1)
|
||||
return false;
|
||||
|
||||
int toGive;
|
||||
int toGet;
|
||||
m->getOffer(mostExpendable, mostWanted, toGive, toGet, EMarketMode::RESOURCE_RESOURCE);
|
||||
//logAi->info("Offer is: I get %d of %s for %d of %s at %s", toGet, mostWanted, toGive, mostExpendable, obj->getObjectName());
|
||||
//TODO trade only as much as needed
|
||||
if (toGive && toGive <= available[mostExpendable]) //don't try to sell 0 resources
|
||||
{
|
||||
cb->trade(m->getObjInstanceID(), EMarketMode::RESOURCE_RESOURCE, GameResID(mostExpendable), GameResID(mostWanted), toGive);
|
||||
#if NKAI_TRACE_LEVEL >= 1
|
||||
logAi->info("Traded %d of %s for %d of %s at %s", toGive, mostExpendable, toGet, mostWanted, obj->getObjectName());
|
||||
#endif
|
||||
haveTraded = true;
|
||||
shouldTryToTrade = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return haveTraded;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -120,13 +120,14 @@ public:
|
||||
ScanDepth getScanDepth() const { return scanDepth; }
|
||||
bool isOpenMap() const { return openMap; }
|
||||
bool isObjectGraphAllowed() const { return useObjectGraph; }
|
||||
bool handleTrading();
|
||||
|
||||
private:
|
||||
void resetAiState();
|
||||
void updateAiState(int pass, bool fast = false);
|
||||
void decompose(Goals::TGoalVec & result, Goals::TSubgoal behavior, int decompositionMaxDepth) 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 areAffectedObjectsPresent(Goals::TTask task) const;
|
||||
HeroRole getTaskRole(Goals::TTask task) const;
|
||||
|
@ -15,6 +15,8 @@
|
||||
#include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h"
|
||||
#include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
|
||||
#include "../../../lib/mapObjects/MapObjects.h"
|
||||
#include "../../../lib/mapping/CMapDefines.h"
|
||||
#include "../../../lib/RoadHandler.h"
|
||||
#include "../../../lib/CCreatureHandler.h"
|
||||
#include "../../../lib/VCMI_Lib.h"
|
||||
#include "../../../lib/StartInfo.h"
|
||||
@ -33,11 +35,9 @@
|
||||
namespace NKAI
|
||||
{
|
||||
|
||||
#define 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
|
||||
const float MIN_CRITICAL_VALUE = 2.0f;
|
||||
constexpr float MIN_CRITICAL_VALUE = 2.0f;
|
||||
|
||||
EvaluationContext::EvaluationContext(const Nullkiller * ai)
|
||||
EvaluationContext::EvaluationContext(const Nullkiller* ai)
|
||||
: movementCost(0.0),
|
||||
manaCost(0),
|
||||
danger(0),
|
||||
@ -51,9 +51,22 @@ EvaluationContext::EvaluationContext(const Nullkiller * ai)
|
||||
heroRole(HeroRole::SCOUT),
|
||||
turn(0),
|
||||
strategicalValue(0),
|
||||
conquestValue(0),
|
||||
evaluator(ai),
|
||||
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 creaturesAreFree = creature->getLevel() == 1;
|
||||
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)
|
||||
{
|
||||
case CArtifact::EartClass::ART_TREASURE:
|
||||
//FALL_THROUGH
|
||||
case CArtifact::EartClass::ART_MINOR:
|
||||
classValue = 1000;
|
||||
break;
|
||||
@ -289,6 +304,8 @@ uint64_t RewardEvaluator::getArmyReward(
|
||||
case Obj::CREATURE_GENERATOR3:
|
||||
case Obj::CREATURE_GENERATOR4:
|
||||
return getDwellingArmyValue(ai->cb.get(), target, checkGold);
|
||||
case Obj::SPELL_SCROLL:
|
||||
//FALL_THROUGH
|
||||
case Obj::ARTIFACT:
|
||||
return evaluateArtifactArmyValue(dynamic_cast<const CGArtifact *>(target)->storedArtifact->getType());
|
||||
case Obj::HERO:
|
||||
@ -479,7 +496,7 @@ uint64_t RewardEvaluator::townArmyGrowth(const CGTownInstance * town) const
|
||||
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()));
|
||||
}
|
||||
@ -581,6 +598,54 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target, cons
|
||||
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
|
||||
{
|
||||
auto rewardable = dynamic_cast<const CRewardableObject *>(hut);
|
||||
@ -705,7 +770,7 @@ int32_t getArmyCost(const CArmedInstance * army)
|
||||
|
||||
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;
|
||||
@ -786,7 +851,9 @@ public:
|
||||
uint64_t armyStrength = heroExchange.getReinforcementArmyStrength(evaluationContext.evaluator.ai);
|
||||
|
||||
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.isExchange = true;
|
||||
}
|
||||
};
|
||||
|
||||
@ -804,6 +871,7 @@ public:
|
||||
|
||||
evaluationContext.armyReward += upgradeValue;
|
||||
evaluationContext.addNonCriticalStrategicalValue(upgradeValue / (float)armyUpgrade.hero->getArmyStrength());
|
||||
evaluationContext.isArmyUpgrade = true;
|
||||
}
|
||||
};
|
||||
|
||||
@ -818,22 +886,46 @@ public:
|
||||
int tilesDiscovered = task->value;
|
||||
|
||||
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
|
||||
{
|
||||
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;
|
||||
|
||||
Goals::StayAtTown & stayAtTown = dynamic_cast<Goals::StayAtTown &>(*task);
|
||||
Goals::StayAtTown& stayAtTown = dynamic_cast<Goals::StayAtTown&>(*task);
|
||||
|
||||
evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero());
|
||||
evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted();
|
||||
evaluationContext.movementCost += stayAtTown.getMovementWasted();
|
||||
if (evaluationContext.armyReward == 0)
|
||||
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)
|
||||
{
|
||||
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.threat, enemyDanger.threat);
|
||||
}
|
||||
}
|
||||
|
||||
@ -896,6 +981,10 @@ public:
|
||||
else
|
||||
evaluationContext.addNonCriticalStrategicalValue(1.7f * multiplier * strategicalValue);
|
||||
|
||||
evaluationContext.defenseValue = town->fortLevel();
|
||||
evaluationContext.isDefend = true;
|
||||
evaluationContext.threatTurns = treat.turn;
|
||||
|
||||
vstd::amax(evaluationContext.danger, defendTown.getTreat().danger);
|
||||
addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength());
|
||||
}
|
||||
@ -926,6 +1015,8 @@ public:
|
||||
for(auto & node : path.nodes)
|
||||
{
|
||||
vstd::amax(costsPerHero[node.targetHero], node.cost);
|
||||
if (node.layer == EPathfindingLayer::SAIL)
|
||||
evaluationContext.involvesSailing = true;
|
||||
}
|
||||
|
||||
for(auto pair : costsPerHero)
|
||||
@ -952,10 +1043,18 @@ public:
|
||||
evaluationContext.armyGrowth += evaluationContext.evaluator.getArmyGrowth(target, hero, army);
|
||||
evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, heroRole);
|
||||
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.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());
|
||||
vstd::amax(evaluationContext.turn, path.turn());
|
||||
}
|
||||
@ -996,6 +1095,7 @@ public:
|
||||
evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold) / boost;
|
||||
evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, role) / boost;
|
||||
evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target) / boost);
|
||||
evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target);
|
||||
evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army) / boost;
|
||||
evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost;
|
||||
evaluationContext.movementCost += objInfo.second.movementCost / boost;
|
||||
@ -1021,6 +1121,14 @@ public:
|
||||
Goals::ExchangeSwapTownHeroes & swapCommand = dynamic_cast<Goals::ExchangeSwapTownHeroes &>(*task);
|
||||
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)
|
||||
{
|
||||
auto defenderRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(garrisonHero);
|
||||
@ -1029,6 +1137,9 @@ public:
|
||||
evaluationContext.movementCost += mpLeft;
|
||||
evaluationContext.movementCostByRole[defenderRole] += mpLeft;
|
||||
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.heroRole = HeroRole::MAIN;
|
||||
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.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)
|
||||
{
|
||||
@ -1100,7 +1217,18 @@ public:
|
||||
else if(bi.id >= BuildingID::MAGES_GUILD_1 && bi.id <= BuildingID::MAGES_GUILD_5)
|
||||
{
|
||||
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)
|
||||
{
|
||||
@ -1162,6 +1290,7 @@ EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal
|
||||
for(auto subgoal : parts)
|
||||
{
|
||||
context.goldCost += subgoal->goldCost;
|
||||
context.buildingCost += subgoal->buildingCost;
|
||||
|
||||
for(auto builder : evaluationContextBuilders)
|
||||
{
|
||||
@ -1172,7 +1301,7 @@ EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal
|
||||
return context;
|
||||
}
|
||||
|
||||
float PriorityEvaluator::evaluate(Goals::TSubgoal task)
|
||||
float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
|
||||
{
|
||||
auto evaluationContext = buildEvaluationContext(task);
|
||||
|
||||
@ -1185,36 +1314,257 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
|
||||
|
||||
double result = 0;
|
||||
|
||||
try
|
||||
if (ai->settings->isUseFuzzy())
|
||||
{
|
||||
armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage);
|
||||
heroRoleVariable->setValue(evaluationContext.heroRole);
|
||||
mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]);
|
||||
scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]);
|
||||
goldRewardVariable->setValue(goldRewardPerTurn);
|
||||
armyRewardVariable->setValue(evaluationContext.armyReward);
|
||||
armyGrowthVariable->setValue(evaluationContext.armyGrowth);
|
||||
skillRewardVariable->setValue(evaluationContext.skillReward);
|
||||
dangerVariable->setValue(evaluationContext.danger);
|
||||
rewardTypeVariable->setValue(rewardType);
|
||||
closestHeroRatioVariable->setValue(evaluationContext.closestWayRatio);
|
||||
strategicalValueVariable->setValue(evaluationContext.strategicalValue);
|
||||
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);
|
||||
float fuzzyResult = 0;
|
||||
try
|
||||
{
|
||||
armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage);
|
||||
heroRoleVariable->setValue(evaluationContext.heroRole);
|
||||
mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]);
|
||||
scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]);
|
||||
goldRewardVariable->setValue(goldRewardPerTurn);
|
||||
armyRewardVariable->setValue(evaluationContext.armyReward);
|
||||
armyGrowthVariable->setValue(evaluationContext.armyGrowth);
|
||||
skillRewardVariable->setValue(evaluationContext.skillReward);
|
||||
dangerVariable->setValue(evaluationContext.danger);
|
||||
rewardTypeVariable->setValue(rewardType);
|
||||
closestHeroRatioVariable->setValue(evaluationContext.closestWayRatio);
|
||||
strategicalValueVariable->setValue(evaluationContext.strategicalValue);
|
||||
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
|
||||
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(),
|
||||
evaluationContext.armyLossPersentage,
|
||||
(int)evaluationContext.turn,
|
||||
@ -1223,9 +1573,14 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
|
||||
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,
|
||||
result);
|
||||
|
@ -41,6 +41,7 @@ public:
|
||||
float getResourceRequirementStrength(int resType) const;
|
||||
float getResourceRequirementStrength(const TResources & res) const;
|
||||
float getStrategicalValue(const CGObjectInstance * target, const CGHeroInstance * hero = nullptr) const;
|
||||
float getConquestValue(const CGObjectInstance* target) const;
|
||||
float getTotalResourceRequirementStrength(int resType) const;
|
||||
float evaluateWitchHutSkillScore(const CGObjectInstance * hut, 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;
|
||||
const HitMapInfo & getEnemyHeroDanger(const int3 & tile, uint8_t turn) 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
|
||||
@ -65,10 +66,24 @@ struct DLL_EXPORT EvaluationContext
|
||||
int32_t goldCost;
|
||||
float skillReward;
|
||||
float strategicalValue;
|
||||
float conquestValue;
|
||||
HeroRole heroRole;
|
||||
uint8_t turn;
|
||||
RewardEvaluator evaluator;
|
||||
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);
|
||||
|
||||
@ -91,7 +106,20 @@ public:
|
||||
~PriorityEvaluator();
|
||||
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:
|
||||
const Nullkiller * ai;
|
||||
|
@ -11,6 +11,8 @@
|
||||
#include <limits>
|
||||
|
||||
#include "Settings.h"
|
||||
|
||||
#include "../../../lib/constants/StringConstants.h"
|
||||
#include "../../../lib/mapObjectConstructors/AObjectTypeHandler.h"
|
||||
#include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h"
|
||||
#include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
|
||||
@ -22,56 +24,42 @@
|
||||
|
||||
namespace NKAI
|
||||
{
|
||||
Settings::Settings()
|
||||
Settings::Settings(int difficultyLevel)
|
||||
: maxRoamingHeroes(8),
|
||||
mainHeroTurnDistanceLimit(10),
|
||||
scoutHeroTurnDistanceLimit(5),
|
||||
maxGoldPressure(0.3f),
|
||||
maxGoldPressure(0.3f),
|
||||
retreatThresholdRelative(0.3),
|
||||
retreatThresholdAbsolute(10000),
|
||||
safeAttackRatio(1.1),
|
||||
maxpass(10),
|
||||
pathfinderBucketsCount(1),
|
||||
pathfinderBucketSize(32),
|
||||
allowObjectGraph(true),
|
||||
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.Struct()["maxRoamingHeroes"].Integer();
|
||||
}
|
||||
|
||||
if(node.Struct()["mainHeroTurnDistanceLimit"].isNumber())
|
||||
{
|
||||
mainHeroTurnDistanceLimit = node.Struct()["mainHeroTurnDistanceLimit"].Integer();
|
||||
}
|
||||
|
||||
if(node.Struct()["scoutHeroTurnDistanceLimit"].isNumber())
|
||||
{
|
||||
scoutHeroTurnDistanceLimit = node.Struct()["scoutHeroTurnDistanceLimit"].Integer();
|
||||
}
|
||||
|
||||
if(node.Struct()["maxpass"].isNumber())
|
||||
{
|
||||
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();
|
||||
}
|
||||
maxRoamingHeroes = node["maxRoamingHeroes"].Integer();
|
||||
mainHeroTurnDistanceLimit = node["mainHeroTurnDistanceLimit"].Integer();
|
||||
scoutHeroTurnDistanceLimit = node["scoutHeroTurnDistanceLimit"].Integer();
|
||||
maxpass = node["maxpass"].Integer();
|
||||
pathfinderBucketsCount = node["pathfinderBucketsCount"].Integer();
|
||||
pathfinderBucketSize = node["pathfinderBucketSize"].Integer();
|
||||
maxGoldPressure = node["maxGoldPressure"].Float();
|
||||
retreatThresholdRelative = node["retreatThresholdRelative"].Float();
|
||||
retreatThresholdAbsolute = node["retreatThresholdAbsolute"].Float();
|
||||
maxArmyLossTarget = node["maxArmyLossTarget"].Float();
|
||||
safeAttackRatio = node["safeAttackRatio"].Float();
|
||||
allowObjectGraph = node["allowObjectGraph"].Bool();
|
||||
updateHitmapOnTileReveal = node["updateHitmapOnTileReveal"].Bool();
|
||||
openMap = node["openMap"].Bool();
|
||||
useFuzzy = node["useFuzzy"].Bool();
|
||||
useTroopsFromGarrisons = node["useTroopsFromGarrisons"].Bool();
|
||||
}
|
||||
}
|
||||
|
@ -25,21 +25,37 @@ namespace NKAI
|
||||
int mainHeroTurnDistanceLimit;
|
||||
int scoutHeroTurnDistanceLimit;
|
||||
int maxpass;
|
||||
int pathfinderBucketsCount;
|
||||
int pathfinderBucketSize;
|
||||
float maxGoldPressure;
|
||||
float retreatThresholdRelative;
|
||||
float retreatThresholdAbsolute;
|
||||
float safeAttackRatio;
|
||||
float maxArmyLossTarget;
|
||||
bool allowObjectGraph;
|
||||
bool useTroopsFromGarrisons;
|
||||
bool updateHitmapOnTileReveal;
|
||||
bool openMap;
|
||||
bool useFuzzy;
|
||||
|
||||
public:
|
||||
Settings();
|
||||
explicit Settings(int difficultyLevel);
|
||||
|
||||
int getMaxPass() const { return maxpass; }
|
||||
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 getMainHeroTurnDistanceLimit() const { return mainHeroTurnDistanceLimit; }
|
||||
int getScoutHeroTurnDistanceLimit() const { return scoutHeroTurnDistanceLimit; }
|
||||
int getPathfinderBucketsCount() const { return pathfinderBucketsCount; }
|
||||
int getPathfinderBucketSize() const { return pathfinderBucketSize; }
|
||||
bool isObjectGraphAllowed() const { return allowObjectGraph; }
|
||||
bool isGarrisonTroopsUsageAllowed() const { return useTroopsFromGarrisons; }
|
||||
bool isUpdateHitmapOnTileReveal() const { return updateHitmapOnTileReveal; }
|
||||
bool isOpenMap() const { return openMap; }
|
||||
bool isUseFuzzy() const { return useFuzzy; }
|
||||
};
|
||||
}
|
||||
|
@ -104,6 +104,7 @@ namespace Goals
|
||||
bool isAbstract; SETTER(bool, isAbstract)
|
||||
int value; SETTER(int, value)
|
||||
ui64 goldCost; SETTER(ui64, goldCost)
|
||||
TResources buildingCost; SETTER(TResources, buildingCost)
|
||||
int resID; SETTER(int, resID)
|
||||
int objid; SETTER(int, objid)
|
||||
int aid; SETTER(int, aid)
|
||||
|
@ -53,6 +53,9 @@ void AdventureSpellCast::accept(AIGateway * ai)
|
||||
throw cannotFulfillGoalException("The town is already occupied by " + town->visitingHero->getNameTranslated());
|
||||
}
|
||||
|
||||
if (hero->inTownGarrison)
|
||||
ai->myCb->swapGarrisonHero(hero->visitedTown);
|
||||
|
||||
auto wait = cb->waitTillRealize;
|
||||
|
||||
cb->waitTillRealize = true;
|
||||
|
@ -90,9 +90,12 @@ void ExchangeSwapTownHeroes::accept(AIGateway * ai)
|
||||
|
||||
if(!town->garrisonHero)
|
||||
{
|
||||
while(upperArmy->stacksCount() != 0)
|
||||
if (!garrisonHero->canBeMergedWith(*town))
|
||||
{
|
||||
cb->dismissCreature(upperArmy, upperArmy->Slots().begin()->first);
|
||||
while (upperArmy->stacksCount() != 0)
|
||||
{
|
||||
cb->dismissCreature(upperArmy, upperArmy->Slots().begin()->first);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -22,6 +22,7 @@ ExecuteHeroChain::ExecuteHeroChain(const AIPath & path, const CGObjectInstance *
|
||||
{
|
||||
hero = path.targetHero;
|
||||
tile = path.targetTile();
|
||||
closestWayRatio = 1;
|
||||
|
||||
if(obj)
|
||||
{
|
||||
@ -85,6 +86,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
|
||||
|
||||
ai->nullkiller->setActive(chainPath.targetHero, tile);
|
||||
ai->nullkiller->setTargetObject(objid);
|
||||
ai->nullkiller->objectClusterizer->reset();
|
||||
|
||||
auto targetObject = ai->myCb->getObj(static_cast<ObjectInstanceID>(objid), false);
|
||||
|
||||
|
@ -73,6 +73,7 @@ void RecruitHero::accept(AIGateway * ai)
|
||||
std::unique_lock lockGuard(ai->nullkiller->aiStateMutex);
|
||||
|
||||
ai->nullkiller->heroManager->update();
|
||||
ai->nullkiller->objectClusterizer->reset();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -44,6 +44,7 @@ namespace Goals
|
||||
}
|
||||
|
||||
std::string toString() const override;
|
||||
const CGHeroInstance* getHero() const override { return heroToBuy; }
|
||||
void accept(AIGateway * ai) override;
|
||||
};
|
||||
}
|
||||
|
@ -36,16 +36,12 @@ std::string StayAtTown::toString() const
|
||||
{
|
||||
return "Stay at town " + town->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)
|
||||
{
|
||||
if(hero->visitedTown != town)
|
||||
{
|
||||
logAi->error("Hero %s expected visiting town %s", hero->getNameTranslated(), town->getNameTranslated());
|
||||
}
|
||||
|
||||
ai->nullkiller->lockHero(hero, HeroLockedReason::DEFENCE);
|
||||
}
|
||||
|
||||
|
@ -175,7 +175,7 @@ void ExplorationHelper::scanTile(const int3 & tile)
|
||||
continue;
|
||||
}
|
||||
|
||||
if(isSafeToVisit(hero, path.heroArmy, path.getTotalDanger()))
|
||||
if(isSafeToVisit(hero, path.heroArmy, path.getTotalDanger(), ai->settings->getSafeAttackRatio()))
|
||||
{
|
||||
bestGoal = goal;
|
||||
bestValue = ourValue;
|
||||
|
@ -39,17 +39,17 @@ const uint64_t CHAIN_MAX_DEPTH = 4;
|
||||
|
||||
const bool DO_NOT_SAVE_TO_COMMITTED_TILES = false;
|
||||
|
||||
AISharedStorage::AISharedStorage(int3 sizes)
|
||||
AISharedStorage::AISharedStorage(int3 sizes, int numChains)
|
||||
{
|
||||
if(!shared){
|
||||
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;
|
||||
|
||||
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];
|
||||
|
||||
@ -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)
|
||||
: 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>>(
|
||||
boost::extents[sizes.z][sizes.x][sizes.y][EPathfindingLayer::NUM_LAYERS]);
|
||||
@ -169,8 +179,8 @@ std::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
|
||||
const EPathfindingLayer layer,
|
||||
const ChainActor * actor)
|
||||
{
|
||||
int bucketIndex = ((uintptr_t)actor + static_cast<uint32_t>(layer)) % AIPathfinding::BUCKET_COUNT;
|
||||
int bucketOffset = bucketIndex * AIPathfinding::BUCKET_SIZE;
|
||||
int bucketIndex = ((uintptr_t)actor + static_cast<uint32_t>(layer)) % ai->settings->getPathfinderBucketsCount();
|
||||
int bucketOffset = bucketIndex * ai->settings->getPathfinderBucketSize();
|
||||
auto chains = nodes.get(pos);
|
||||
|
||||
if(blocked(pos, layer))
|
||||
@ -178,7 +188,7 @@ std::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
|
||||
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];
|
||||
|
||||
@ -486,8 +496,8 @@ public:
|
||||
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.reserve(AIPathfinding::NUM_CHAINS);
|
||||
newChains.reserve(AIPathfinding::NUM_CHAINS);
|
||||
existingChains.reserve(storage.getBucketCount() * storage.getBucketSize());
|
||||
newChains.reserve(storage.getBucketCount() * storage.getBucketSize());
|
||||
}
|
||||
|
||||
void execute(const tbb::blocked_range<size_t>& r)
|
||||
@ -719,6 +729,7 @@ void HeroChainCalculationTask::calculateHeroChain(
|
||||
if(node->action == EPathNodeAction::BATTLE
|
||||
|| node->action == EPathNodeAction::TELEPORT_BATTLE
|
||||
|| node->action == EPathNodeAction::TELEPORT_NORMAL
|
||||
|| node->action == EPathNodeAction::DISEMBARK
|
||||
|| node->action == EPathNodeAction::TELEPORT_BLOCKING_VISIT)
|
||||
{
|
||||
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
|
||||
if(hero.first->getOwner() == ai->playerID
|
||||
&& hero.first->inTownGarrison
|
||||
&& (ai->isHeroLocked(hero.first) || ai->heroManager->heroCapReached()))
|
||||
&& (ai->isHeroLocked(hero.first) || ai->heroManager->heroCapReached(false)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
@ -1196,6 +1207,11 @@ void AINodeStorage::calculateTownPortal(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (targetTown->visitingHero
|
||||
&& (targetTown->visitingHero.get()->getFactionID() != actor->hero->getFactionID()
|
||||
|| targetTown->getUpperArmy()->stacksCount()))
|
||||
continue;
|
||||
|
||||
auto nodeOptional = townPortalFinder.createTownPortalNode(targetTown);
|
||||
|
||||
if(nodeOptional)
|
||||
@ -1418,6 +1434,10 @@ void AINodeStorage::calculateChainInfo(std::vector<AIPath> & paths, const int3 &
|
||||
path.heroArmy = node.actor->creatureSet;
|
||||
path.armyLoss = node.armyLoss;
|
||||
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)
|
||||
{
|
||||
@ -1564,7 +1584,7 @@ uint8_t AIPath::turn() const
|
||||
|
||||
uint64_t AIPath::getHeroStrength() const
|
||||
{
|
||||
return targetHero->getFightingStrength() * getHeroArmyStrengthWithCommander(targetHero, heroArmy);
|
||||
return targetHero->getHeroStrength() * getHeroArmyStrengthWithCommander(targetHero, heroArmy);
|
||||
}
|
||||
|
||||
uint64_t AIPath::getTotalDanger() const
|
||||
|
@ -29,9 +29,6 @@ namespace NKAI
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
@ -157,7 +154,7 @@ public:
|
||||
static boost::mutex locker;
|
||||
static uint32_t version;
|
||||
|
||||
AISharedStorage(int3 mapSize);
|
||||
AISharedStorage(int3 sizes, int numChains);
|
||||
~AISharedStorage();
|
||||
|
||||
STRONG_INLINE
|
||||
@ -197,6 +194,9 @@ public:
|
||||
bool selectFirstActor();
|
||||
bool selectNextActor();
|
||||
|
||||
int getBucketCount() const;
|
||||
int getBucketSize() const;
|
||||
|
||||
std::vector<CGPathNode *> getInitialNodes() override;
|
||||
|
||||
virtual void calculateNeighbours(
|
||||
@ -298,7 +298,7 @@ public:
|
||||
|
||||
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);
|
||||
|
@ -46,7 +46,7 @@ ChainActor::ChainActor(const CGHeroInstance * hero, HeroRole heroRole, uint64_t
|
||||
initialMovement = hero->movementPointsRemaining();
|
||||
initialTurn = 0;
|
||||
armyValue = getHeroArmyStrengthWithCommander(hero, hero);
|
||||
heroFightingStrength = hero->getFightingStrength();
|
||||
heroFightingStrength = hero->getHeroStrength();
|
||||
tiCache.reset(new TurnInfo(hero));
|
||||
}
|
||||
|
||||
|
@ -25,11 +25,9 @@ using crstring = const std::string &;
|
||||
using dwellingContent = std::pair<ui32, std::vector<CreatureID>>;
|
||||
|
||||
const int ACTUAL_RESOURCE_COUNT = 7;
|
||||
const int ALLOWED_ROAMING_HEROES = 8;
|
||||
|
||||
//implementation-dependent
|
||||
extern const double SAFE_ATTACK_CONSTANT;
|
||||
extern const int GOLD_RESERVE;
|
||||
|
||||
extern thread_local CCallback * cb;
|
||||
extern thread_local VCAI * ai;
|
||||
|
@ -14,8 +14,6 @@
|
||||
#include "../../CCallback.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)
|
||||
: resources(Res), goal(Goal)
|
||||
{
|
||||
|
@ -1314,8 +1314,6 @@ bool VCAI::canRecruitAnyHero(const CGTownInstance * t) const
|
||||
return false;
|
||||
if(cb->getResourceAmount(EGameResID::GOLD) < GameConstants::HERO_GOLD_COST) //TODO: use ResourceManager
|
||||
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))
|
||||
return false;
|
||||
if(!cb->getAvailableHeroes(t).size())
|
||||
|
278
CI/example.markdownlint-cli2.jsonc
Normal file
@ -0,0 +1,278 @@
|
||||
{
|
||||
"config" : {
|
||||
"default" : true,
|
||||
|
||||
// MD001/heading-increment : Heading levels should only increment by one level at a time : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md001.md
|
||||
"MD001": false,
|
||||
|
||||
// MD003/heading-style : Heading style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md003.md
|
||||
"MD003": {
|
||||
"style": "atx"
|
||||
},
|
||||
|
||||
// MD004/ul-style : Unordered list style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md004.md
|
||||
"MD004": false,
|
||||
// FIXME: enable and consider fixing
|
||||
//{
|
||||
// "style": "consistent"
|
||||
//},
|
||||
|
||||
// MD005/list-indent : Inconsistent indentation for list items at the same level : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md005.md
|
||||
"MD005": true,
|
||||
|
||||
// MD007/ul-indent : Unordered list indentation : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md007.md
|
||||
"MD007": {
|
||||
// Spaces for indent
|
||||
"indent": 2,
|
||||
// Whether to indent the first level of the list
|
||||
"start_indented": false,
|
||||
// Spaces for first level indent (when start_indented is set)
|
||||
"start_indent": 0
|
||||
},
|
||||
|
||||
// MD009/no-trailing-spaces : Trailing spaces : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md009.md
|
||||
"MD009": {
|
||||
// Spaces for line break
|
||||
"br_spaces": 2,
|
||||
// Allow spaces for empty lines in list items
|
||||
"list_item_empty_lines": false,
|
||||
// Include unnecessary breaks
|
||||
"strict": false
|
||||
},
|
||||
|
||||
// MD010/no-hard-tabs : Hard tabs : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md010.md
|
||||
"MD010": {
|
||||
// Include code blocks
|
||||
"code_blocks": false,
|
||||
// Fenced code languages to ignore
|
||||
"ignore_code_languages": [],
|
||||
// Number of spaces for each hard tab
|
||||
"spaces_per_tab": 4
|
||||
},
|
||||
|
||||
// MD011/no-reversed-links : Reversed link syntax : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md011.md
|
||||
"MD011": true,
|
||||
|
||||
// MD012/no-multiple-blanks : Multiple consecutive blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md012.md
|
||||
"MD012": {
|
||||
// Consecutive blank lines
|
||||
"maximum": 1
|
||||
},
|
||||
|
||||
// MD013/line-length : Line length : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md013.md
|
||||
"MD013": false,
|
||||
|
||||
// MD014/commands-show-output : Dollar signs used before commands without showing output : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md014.md
|
||||
"MD014": true,
|
||||
|
||||
// MD018/no-missing-space-atx : No space after hash on atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md018.md
|
||||
"MD018": true,
|
||||
|
||||
// MD019/no-multiple-space-atx : Multiple spaces after hash on atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md019.md
|
||||
"MD019": true,
|
||||
|
||||
// MD020/no-missing-space-closed-atx : No space inside hashes on closed atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md020.md
|
||||
"MD020": true,
|
||||
|
||||
// MD021/no-multiple-space-closed-atx : Multiple spaces inside hashes on closed atx style heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md021.md
|
||||
"MD021": true,
|
||||
|
||||
// MD022/blanks-around-headings : Headings should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md022.md
|
||||
"MD022": {
|
||||
// Blank lines above heading
|
||||
"lines_above": 1,
|
||||
// Blank lines below heading
|
||||
"lines_below": 1
|
||||
},
|
||||
|
||||
// MD023/heading-start-left : Headings must start at the beginning of the line : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md023.md
|
||||
"MD023": true,
|
||||
|
||||
// MD024/no-duplicate-heading : Multiple headings with the same content : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md024.md
|
||||
"MD024": false,
|
||||
// FIXME: false positives?
|
||||
//{
|
||||
// // Only check sibling headings
|
||||
// "allow_different_nesting": true,
|
||||
// // Only check sibling headings
|
||||
// "siblings_only": true
|
||||
//},
|
||||
|
||||
// MD025/single-title/single-h1 : Multiple top-level headings in the same document : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md025.md
|
||||
"MD025": {
|
||||
// Heading level
|
||||
"level": 1,
|
||||
// RegExp for matching title in front matter
|
||||
"front_matter_title": "^\\s*title\\s*[:=]"
|
||||
},
|
||||
|
||||
// MD026/no-trailing-punctuation : Trailing punctuation in heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md026.md
|
||||
"MD026": {
|
||||
// Punctuation characters
|
||||
"punctuation": ".,;:!。,;:!"
|
||||
},
|
||||
|
||||
// MD027/no-multiple-space-blockquote : Multiple spaces after blockquote symbol : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md027.md
|
||||
"MD027": true,
|
||||
|
||||
// MD028/no-blanks-blockquote : Blank line inside blockquote : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md028.md
|
||||
"MD028": true,
|
||||
|
||||
// MD029/ol-prefix : Ordered list item prefix : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md029.md
|
||||
"MD029": false,
|
||||
// FIXME: false positives or broken formatting
|
||||
//{
|
||||
// // List style
|
||||
// "style": "ordered"
|
||||
//},
|
||||
|
||||
// MD030/list-marker-space : Spaces after list markers : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md030.md
|
||||
"MD030": {
|
||||
// Spaces for single-line unordered list items
|
||||
"ul_single": 1,
|
||||
// Spaces for single-line ordered list items
|
||||
"ol_single": 1,
|
||||
// Spaces for multi-line unordered list items
|
||||
"ul_multi": 1,
|
||||
// Spaces for multi-line ordered list items
|
||||
"ol_multi": 1
|
||||
},
|
||||
|
||||
// MD031/blanks-around-fences : Fenced code blocks should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md031.md
|
||||
"MD031": {
|
||||
// Include list items
|
||||
"list_items": false
|
||||
},
|
||||
|
||||
// MD032/blanks-around-lists : Lists should be surrounded by blank lines : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md032.md
|
||||
"MD032": true,
|
||||
|
||||
// MD033/no-inline-html : Inline HTML : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md033.md
|
||||
"MD033": false,
|
||||
// FIXME: enable and consider fixing
|
||||
//{
|
||||
// // Allowed elements
|
||||
// "allowed_elements": []
|
||||
//},
|
||||
|
||||
// MD034/no-bare-urls : Bare URL used : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md034.md
|
||||
"MD034": true,
|
||||
|
||||
// MD035/hr-style : Horizontal rule style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md035.md
|
||||
"MD035": {
|
||||
// Horizontal rule style
|
||||
"style": "consistent"
|
||||
},
|
||||
|
||||
// MD036/no-emphasis-as-heading : Emphasis used instead of a heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md036.md
|
||||
"MD036": false,
|
||||
// FIXME: enable and consider fixing
|
||||
// {
|
||||
// // Punctuation characters
|
||||
// "punctuation": ".,;:!?。,;:!?"
|
||||
// },
|
||||
|
||||
// MD037/no-space-in-emphasis : Spaces inside emphasis markers : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md037.md
|
||||
"MD037": true,
|
||||
|
||||
// MD038/no-space-in-code : Spaces inside code span elements : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md038.md
|
||||
"MD038": true,
|
||||
|
||||
// MD039/no-space-in-links : Spaces inside link text : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md039.md
|
||||
"MD039": true,
|
||||
|
||||
// MD040/fenced-code-language : Fenced code blocks should have a language specified : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md040.md
|
||||
"MD040": {
|
||||
// List of languages
|
||||
"allowed_languages": [ "cpp", "json", "sh", "text", "nix", "powershell", "lua" ],
|
||||
// Require language only
|
||||
"language_only": true
|
||||
},
|
||||
|
||||
// MD041/first-line-heading/first-line-h1 : First line in a file should be a top-level heading : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md041.md
|
||||
"MD041": {
|
||||
// Heading level
|
||||
"level": 1,
|
||||
// RegExp for matching title in front matter
|
||||
"front_matter_title": "^\\s*title\\s*[:=]"
|
||||
},
|
||||
|
||||
// MD042/no-empty-links : No empty links : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md042.md
|
||||
"MD042": true,
|
||||
|
||||
// MD043/required-headings : Required heading structure : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md043.md
|
||||
"MD043": false,
|
||||
|
||||
// MD044/proper-names : Proper names should have the correct capitalization : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md044.md
|
||||
"MD044": false,
|
||||
|
||||
// MD045/no-alt-text : Images should have alternate text (alt text) : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md045.md
|
||||
"MD045": false,
|
||||
|
||||
// MD046/code-block-style : Code block style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md046.md
|
||||
"MD046": {
|
||||
// Block style
|
||||
"style": "fenced"
|
||||
},
|
||||
|
||||
// MD047/single-trailing-newline : Files should end with a single newline character : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md047.md
|
||||
"MD047": true,
|
||||
|
||||
// MD048/code-fence-style : Code fence style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md048.md
|
||||
"MD048": {
|
||||
// Code fence style
|
||||
"style": "backtick"
|
||||
},
|
||||
|
||||
// MD049/emphasis-style : Emphasis style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md049.md
|
||||
"MD049": {
|
||||
// Emphasis style
|
||||
"style": "asterisk"
|
||||
},
|
||||
|
||||
// MD050/strong-style : Strong style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md050.md
|
||||
"MD050": {
|
||||
// Strong style
|
||||
"style": "asterisk"
|
||||
},
|
||||
|
||||
|
||||
|
||||
// MD051/link-fragments : Link fragments should be valid : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md051.md
|
||||
"MD051": true,
|
||||
|
||||
// MD052/reference-links-images : Reference links and images should use a label that is defined : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md052.md
|
||||
"MD052": {
|
||||
// Include shortcut syntax
|
||||
"shortcut_syntax": false
|
||||
},
|
||||
|
||||
// MD053/link-image-reference-definitions : Link and image reference definitions should be needed : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md053.md
|
||||
"MD053": {
|
||||
// Ignored definitions
|
||||
"ignored_definitions": [
|
||||
"//"
|
||||
]
|
||||
},
|
||||
|
||||
// MD054/link-image-style : Link and image style : https://github.com/DavidAnson/markdownlint/blob/v0.32.1/doc/md054.md
|
||||
"MD054": {
|
||||
// Allow autolinks
|
||||
"autolink": true,
|
||||
// Allow inline links and images
|
||||
"inline": true,
|
||||
// Allow full reference links and images
|
||||
"full": true,
|
||||
// Allow collapsed reference links and images
|
||||
"collapsed": true,
|
||||
// Allow shortcut reference links and images
|
||||
"shortcut": true,
|
||||
// Allow URLs as inline links
|
||||
"url_inline": true
|
||||
},
|
||||
|
||||
// MD058 - Tables should be surrounded by blank lines
|
||||
"MD058" : true
|
||||
|
||||
}
|
||||
}
|
1660
ChangeLog.md
Before Width: | Height: | Size: 5.5 KiB After Width: | Height: | Size: 5.5 KiB |
Before Width: | Height: | Size: 11 KiB After Width: | Height: | Size: 11 KiB |
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 2.3 KiB |
Before Width: | Height: | Size: 2.0 KiB After Width: | Height: | Size: 2.0 KiB |
Before Width: | Height: | Size: 422 B After Width: | Height: | Size: 422 B |
Before Width: | Height: | Size: 272 B After Width: | Height: | Size: 272 B |
BIN
Mods/vcmi/Content/Sprites/battle/queueDefend.png
Normal file
After Width: | Height: | Size: 594 B |
BIN
Mods/vcmi/Content/Sprites/battle/queueWait.png
Normal file
After Width: | Height: | Size: 6.6 KiB |
Before Width: | Height: | Size: 139 B After Width: | Height: | Size: 139 B |
Before Width: | Height: | Size: 457 B After Width: | Height: | Size: 457 B |
Before Width: | Height: | Size: 184 B After Width: | Height: | Size: 184 B |
Before Width: | Height: | Size: 307 B After Width: | Height: | Size: 307 B |
Before Width: | Height: | Size: 245 B After Width: | Height: | Size: 245 B |
Before Width: | Height: | Size: 222 B After Width: | Height: | Size: 222 B |
Before Width: | Height: | Size: 377 B After Width: | Height: | Size: 377 B |
Before Width: | Height: | Size: 347 B After Width: | Height: | Size: 347 B |
Before Width: | Height: | Size: 139 B After Width: | Height: | Size: 139 B |
Before Width: | Height: | Size: 455 B After Width: | Height: | Size: 455 B |
Before Width: | Height: | Size: 181 B After Width: | Height: | Size: 181 B |
Before Width: | Height: | Size: 305 B After Width: | Height: | Size: 305 B |
Before Width: | Height: | Size: 238 B After Width: | Height: | Size: 238 B |
Before Width: | Height: | Size: 217 B After Width: | Height: | Size: 217 B |
Before Width: | Height: | Size: 370 B After Width: | Height: | Size: 370 B |
Before Width: | Height: | Size: 343 B After Width: | Height: | Size: 343 B |
8
Mods/vcmi/Content/Sprites/cprsmall.json
Normal file
@ -0,0 +1,8 @@
|
||||
{
|
||||
"images" :
|
||||
[
|
||||
// Fix for swapped in H3 icons of Wight and Wraith
|
||||
{ "frame" : 62, "defFile" : "cprsmall.def", "defFrame" : 63},
|
||||
{ "frame" : 63, "defFile" : "cprsmall.def", "defFrame" : 62}
|
||||
]
|
||||
}
|
Before Width: | Height: | Size: 111 B After Width: | Height: | Size: 111 B |
Before Width: | Height: | Size: 102 B After Width: | Height: | Size: 102 B |
Before Width: | Height: | Size: 96 B After Width: | Height: | Size: 96 B |
Before Width: | Height: | Size: 112 B After Width: | Height: | Size: 112 B |
Before Width: | Height: | Size: 133 B After Width: | Height: | Size: 133 B |
Before Width: | Height: | Size: 2.8 KiB After Width: | Height: | Size: 2.8 KiB |
Before Width: | Height: | Size: 489 B After Width: | Height: | Size: 489 B |
Before Width: | Height: | Size: 487 B After Width: | Height: | Size: 487 B |
Before Width: | Height: | Size: 401 B After Width: | Height: | Size: 401 B |
Before Width: | Height: | Size: 522 B After Width: | Height: | Size: 522 B |
Before Width: | Height: | Size: 374 B After Width: | Height: | Size: 374 B |
Before Width: | Height: | Size: 997 B After Width: | Height: | Size: 997 B |
Before Width: | Height: | Size: 986 B After Width: | Height: | Size: 986 B |