1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-04-15 11:46:56 +02:00

Merge remote-tracking branch 'origin/develop' into fix_rmg_teams

# Conflicts:
#	client/lobby/RandomMapTab.cpp
This commit is contained in:
Tomasz Zieliński 2023-10-26 10:31:41 +02:00
commit 36911d1e0a
1271 changed files with 148868 additions and 117165 deletions

3
.gitattributes vendored Normal file
View File

@ -0,0 +1,3 @@
*.json linguist-language=JSON-with-Comments
*.h linguist-language=C++
*.cpp linguist-language=C++

View File

@ -103,7 +103,7 @@ jobs:
test: 0
pack: 1
extension: ipa
preset: ios-release-conan
preset: ios-release-conan-ccache
conan_profile: ios-arm64
conan_options: --options with_apple_system_libs=True
- platform: msvc
@ -111,7 +111,7 @@ jobs:
test: 0
pack: 1
extension: exe
preset: windows-msvc-release
preset: windows-msvc-release-ccache
- platform: mingw-ubuntu
os: ubuntu-22.04
test: 0
@ -145,11 +145,27 @@ jobs:
with:
submodules: recursive
- name: Validate JSON
# the Python yaml module doesn't seem to work on mac-arm
# also, running it on multiple presets is redundant and slightly increases already long CI built times
if: ${{ startsWith(matrix.preset, 'linux-clang-test') }}
run: |
pip3 install json5 jstyleson
python3 CI/linux-qt6/validate_json.py
- name: Dependencies
run: source '${{github.workspace}}/CI/${{matrix.platform}}/before_install.sh'
env:
VCMI_BUILD_PLATFORM: x64
- name: ccache
uses: hendrikmuhs/ccache-action@v1.2
with:
key: ${{ matrix.preset }}
# actual cache takes up less space, at most ~1 GB
max-size: "5G"
verbose: 2
- uses: actions/setup-python@v4
if: "${{ matrix.conan_profile != '' }}"
with:
@ -185,9 +201,9 @@ jobs:
env:
PULL_REQUEST: ${{ github.event.pull_request.number }}
- name: CMake Preset
- name: CMake Preset with ccache
run: |
cmake --preset ${{ matrix.preset }}
cmake -DCMAKE_CXX_COMPILER_LAUNCHER=ccache --preset ${{ matrix.preset }}
- name: Build Preset
run: |

View File

@ -18,17 +18,98 @@ uint64_t averageDmg(const DamageRange & range)
return (range.min + range.max) / 2;
}
void DamageCache::cacheDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr<CBattleInfoCallback> hb)
{
auto damage = averageDmg(hb->battleEstimateDamage(attacker, defender, 0).damage);
damageCache[attacker->unitId()][defender->unitId()] = static_cast<float>(damage) / attacker->getCount();
}
void DamageCache::buildDamageCache(std::shared_ptr<HypotheticBattle> hb, int side)
{
auto stacks = hb->battleGetUnitsIf([=](const battle::Unit * u) -> bool
{
return u->isValidTarget();
});
std::vector<const battle::Unit *> ourUnits, enemyUnits;
for(auto stack : stacks)
{
if(stack->unitSide() == side)
ourUnits.push_back(stack);
else
enemyUnits.push_back(stack);
}
for(auto ourUnit : ourUnits)
{
if(!ourUnit->alive())
continue;
for(auto enemyUnit : enemyUnits)
{
if(enemyUnit->alive())
{
cacheDamage(ourUnit, enemyUnit, hb);
cacheDamage(enemyUnit, ourUnit, hb);
}
}
}
}
int64_t DamageCache::getDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr<CBattleInfoCallback> hb)
{
auto damage = damageCache[attacker->unitId()][defender->unitId()] * attacker->getCount();
if(damage == 0)
{
cacheDamage(attacker, defender, hb);
damage = damageCache[attacker->unitId()][defender->unitId()] * attacker->getCount();
}
return static_cast<int64_t>(damage);
}
int64_t DamageCache::getOriginalDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr<CBattleInfoCallback> hb)
{
if(parent)
{
auto attackerDamageMap = parent->damageCache.find(attacker->unitId());
if(attackerDamageMap != parent->damageCache.end())
{
auto targetDamage = attackerDamageMap->second.find(defender->unitId());
if(targetDamage != attackerDamageMap->second.end())
{
return static_cast<int64_t>(targetDamage->second * attacker->getCount());
}
}
}
return getDamage(attacker, defender, hb);
}
AttackPossibility::AttackPossibility(BattleHex from, BattleHex dest, const BattleAttackInfo & attack)
: from(from), dest(dest), attack(attack)
{
}
int64_t AttackPossibility::damageDiff() const
float AttackPossibility::damageDiff() const
{
return defenderDamageReduce - attackerDamageReduce - collateralDamageReduce + shootersBlockedDmg;
}
int64_t AttackPossibility::attackValue() const
float AttackPossibility::damageDiff(float positiveEffectMultiplier, float negativeEffectMultiplier) const
{
return positiveEffectMultiplier * (defenderDamageReduce + shootersBlockedDmg)
- negativeEffectMultiplier * (attackerDamageReduce + collateralDamageReduce);
}
float AttackPossibility::attackValue() const
{
return damageDiff();
}
@ -38,25 +119,28 @@ int64_t AttackPossibility::attackValue() const
/// Half bounty for kill, half for making damage equal to enemy health
/// Bounty - the killed creature average damage calculated against attacker
/// </summary>
int64_t AttackPossibility::calculateDamageReduce(
float AttackPossibility::calculateDamageReduce(
const battle::Unit * attacker,
const battle::Unit * defender,
uint64_t damageDealt,
const CBattleInfoCallback & cb)
DamageCache & damageCache,
std::shared_ptr<CBattleInfoCallback> state)
{
const float HEALTH_BOUNTY = 0.5;
const float KILL_BOUNTY = 1.0 - HEALTH_BOUNTY;
vstd::amin(damageDealt, defender->getAvailableHealth());
// FIXME: provide distance info for Jousting bonus
auto attackerUnitForMeasurement = attacker;
if(attackerUnitForMeasurement->isTurret())
if(!attackerUnitForMeasurement || attackerUnitForMeasurement->isTurret())
{
auto ourUnits = cb.battleGetUnitsIf([&](const battle::Unit * u) -> bool
auto ourUnits = state->battleGetUnitsIf([&](const battle::Unit * u) -> bool
{
return u->unitSide() == attacker->unitSide() && !u->isTurret();
return u->unitSide() != defender->unitSide()
&& !u->isTurret()
&& u->creatureId() != CreatureID::CATAPULT
&& u->creatureId() != CreatureID::BALLISTA
&& u->creatureId() != CreatureID::FIRST_AID_TENT
&& u->getCount();
});
if(ourUnits.empty())
@ -65,15 +149,28 @@ int64_t AttackPossibility::calculateDamageReduce(
attackerUnitForMeasurement = ourUnits.front();
}
auto enemyDamageBeforeAttack = cb.battleEstimateDamage(defender, attackerUnitForMeasurement, 0);
auto enemiesKilled = damageDealt / defender->getMaxHealth() + (damageDealt % defender->getMaxHealth() >= defender->getFirstHPleft() ? 1 : 0);
auto enemyDamage = averageDmg(enemyDamageBeforeAttack.damage);
auto damagePerEnemy = enemyDamage / (double)defender->getCount();
auto maxHealth = defender->getMaxHealth();
auto availableHealth = defender->getFirstHPleft() + ((defender->getCount() - 1) * maxHealth);
return (int64_t)(damagePerEnemy * (enemiesKilled * KILL_BOUNTY + damageDealt * HEALTH_BOUNTY / (double)defender->getMaxHealth()));
vstd::amin(damageDealt, availableHealth);
auto enemyDamageBeforeAttack = damageCache.getOriginalDamage(defender, attackerUnitForMeasurement, state);
auto enemiesKilled = damageDealt / maxHealth + (damageDealt % maxHealth >= defender->getFirstHPleft() ? 1 : 0);
auto damagePerEnemy = enemyDamageBeforeAttack / (double)defender->getCount();
// lets use cached maxHealth here instead of getAvailableHealth
auto firstUnitHpLeft = (availableHealth - damageDealt) % maxHealth;
auto firstUnitHealthRatio = firstUnitHpLeft == 0 ? 1 : static_cast<float>(firstUnitHpLeft) / maxHealth;
auto firstUnitKillValue = (1 - firstUnitHealthRatio) * (1 - firstUnitHealthRatio);
return damagePerEnemy * (enemiesKilled + firstUnitKillValue * HEALTH_BOUNTY);
}
int64_t AttackPossibility::evaluateBlockedShootersDmg(const BattleAttackInfo & attackInfo, BattleHex hex, const HypotheticBattle & state)
int64_t AttackPossibility::evaluateBlockedShootersDmg(
const BattleAttackInfo & attackInfo,
BattleHex hex,
DamageCache & damageCache,
std::shared_ptr<CBattleInfoCallback> state)
{
int64_t res = 0;
@ -84,10 +181,10 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(const BattleAttackInfo & a
auto hexes = attacker->getSurroundingHexes(hex);
for(BattleHex tile : hexes)
{
auto st = state.battleGetUnitByPos(tile, true);
if(!st || !state.battleMatchOwner(st, attacker))
auto st = state->battleGetUnitByPos(tile, true);
if(!st || !state->battleMatchOwner(st, attacker))
continue;
if(!state.battleCanShoot(st))
if(!state->battleCanShoot(st))
continue;
// FIXME: provide distance info for Jousting bonus
@ -97,8 +194,8 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(const BattleAttackInfo & a
BattleAttackInfo meleeAttackInfo(st, attacker, 0, false);
meleeAttackInfo.defenderPos = hex;
auto rangeDmg = state.battleEstimateDamage(rangeAttackInfo);
auto meleeDmg = state.battleEstimateDamage(meleeAttackInfo);
auto rangeDmg = state->battleEstimateDamage(rangeAttackInfo);
auto meleeDmg = state->battleEstimateDamage(meleeAttackInfo);
int64_t gain = averageDmg(rangeDmg.damage) - averageDmg(meleeDmg.damage) + 1;
res += gain;
@ -107,13 +204,17 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(const BattleAttackInfo & a
return res;
}
AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInfo, BattleHex hex, const HypotheticBattle & state)
AttackPossibility AttackPossibility::evaluate(
const BattleAttackInfo & attackInfo,
BattleHex hex,
DamageCache & damageCache,
std::shared_ptr<CBattleInfoCallback> state)
{
auto attacker = attackInfo.attacker;
auto defender = attackInfo.defender;
const std::string cachingStringBlocksRetaliation = "type_BLOCKS_RETALIATION";
static const auto selectorBlocksRetaliation = Selector::type()(BonusType::BLOCKS_RETALIATION);
const auto attackerSide = state.playerToSide(state.battleGetOwner(attacker));
const auto attackerSide = state->playerToSide(state->battleGetOwner(attacker));
const bool counterAttacksBlocked = attacker->hasBonus(selectorBlocksRetaliation, cachingStringBlocksRetaliation);
AttackPossibility bestAp(hex, BattleHex::INVALID, attackInfo);
@ -141,9 +242,9 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf
std::vector<const battle::Unit*> units;
if (attackInfo.shooting)
units = state.getAttackedBattleUnits(attacker, defHex, true, BattleHex::INVALID);
units = state->getAttackedBattleUnits(attacker, defHex, true, BattleHex::INVALID);
else
units = state.getAttackedBattleUnits(attacker, defHex, false, hex);
units = state->getAttackedBattleUnits(attacker, defHex, false, hex);
// ensure the defender is also affected
bool addDefender = true;
@ -169,10 +270,11 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf
for(int i = 0; i < totalAttacks; i++)
{
int64_t damageDealt, damageReceived, defenderDamageReduce, attackerDamageReduce;
int64_t damageDealt, damageReceived;
float defenderDamageReduce, attackerDamageReduce;
DamageEstimation retaliation;
auto attackDmg = state.battleEstimateDamage(ap.attack, &retaliation);
auto attackDmg = state->battleEstimateDamage(ap.attack, &retaliation);
vstd::amin(attackDmg.damage.min, defenderState->getAvailableHealth());
vstd::amin(attackDmg.damage.max, defenderState->getAvailableHealth());
@ -181,7 +283,7 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf
vstd::amin(retaliation.damage.max, ap.attackerState->getAvailableHealth());
damageDealt = averageDmg(attackDmg.damage);
defenderDamageReduce = calculateDamageReduce(attacker, defender, damageDealt, state);
defenderDamageReduce = calculateDamageReduce(attacker, defender, damageDealt, damageCache, state);
ap.attackerState->afterAttack(attackInfo.shooting, false);
//FIXME: use ranged retaliation
@ -191,11 +293,11 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf
if (!attackInfo.shooting && defenderState->ableToRetaliate() && !counterAttacksBlocked)
{
damageReceived = averageDmg(retaliation.damage);
attackerDamageReduce = calculateDamageReduce(defender, attacker, damageReceived, state);
attackerDamageReduce = calculateDamageReduce(defender, attacker, damageReceived, damageCache, state);
defenderState->afterAttack(attackInfo.shooting, true);
}
bool isEnemy = state.battleMatchOwner(attacker, u);
bool isEnemy = state->battleMatchOwner(attacker, u);
// this includes enemy units as well as attacker units under enemy's mind control
if(isEnemy)
@ -225,7 +327,7 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf
}
// check how much damage we gain from blocking enemy shooters on this hex
bestAp.shootersBlockedDmg = evaluateBlockedShootersDmg(attackInfo, hex, state);
bestAp.shootersBlockedDmg = evaluateBlockedShootersDmg(attackInfo, hex, damageCache, state);
#if BATTLE_TRACE_LEVEL>=1
logAi->trace("BattleAI best AP: %s -> %s at %d from %d, affects %d units: d:%lld a:%lld c:%lld s:%lld",

View File

@ -15,6 +15,22 @@
#define BATTLE_TRACE_LEVEL 0
class DamageCache
{
private:
std::unordered_map<uint32_t, std::unordered_map<uint32_t, float>> damageCache;
DamageCache * parent;
public:
DamageCache() : parent(nullptr) {}
DamageCache(DamageCache * parent) : parent(parent) {}
void cacheDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr<CBattleInfoCallback> hb);
int64_t getDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr<CBattleInfoCallback> hb);
int64_t getOriginalDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr<CBattleInfoCallback> hb);
void buildDamageCache(std::shared_ptr<HypotheticBattle> hb, int side);
};
/// <summary>
/// Evaluate attack value of one particular attack taking into account various effects like
/// retaliation, 2-hex breath, collateral damage, shooters blocked damage
@ -30,24 +46,34 @@ public:
std::vector<std::shared_ptr<battle::CUnitState>> affectedUnits;
int64_t defenderDamageReduce = 0;
int64_t attackerDamageReduce = 0; //usually by counter-attack
int64_t collateralDamageReduce = 0; // friendly fire (usually by two-hex attacks)
float defenderDamageReduce = 0;
float attackerDamageReduce = 0; //usually by counter-attack
float collateralDamageReduce = 0; // friendly fire (usually by two-hex attacks)
int64_t shootersBlockedDmg = 0;
AttackPossibility(BattleHex from, BattleHex dest, const BattleAttackInfo & attack_);
int64_t damageDiff() const;
int64_t attackValue() const;
float damageDiff() const;
float attackValue() const;
float damageDiff(float positiveEffectMultiplier, float negativeEffectMultiplier) const;
static AttackPossibility evaluate(const BattleAttackInfo & attackInfo, BattleHex hex, const HypotheticBattle & state);
static AttackPossibility evaluate(
const BattleAttackInfo & attackInfo,
BattleHex hex,
DamageCache & damageCache,
std::shared_ptr<CBattleInfoCallback> state);
static int64_t calculateDamageReduce(
static float calculateDamageReduce(
const battle::Unit * attacker,
const battle::Unit * defender,
uint64_t damageDealt,
const CBattleInfoCallback & cb);
DamageCache & damageCache,
std::shared_ptr<CBattleInfoCallback> cb);
private:
static int64_t evaluateBlockedShootersDmg(const BattleAttackInfo & attackInfo, BattleHex hex, const HypotheticBattle & state);
static int64_t evaluateBlockedShootersDmg(
const BattleAttackInfo & attackInfo,
BattleHex hex,
DamageCache & damageCache,
std::shared_ptr<CBattleInfoCallback> state);
};

File diff suppressed because it is too large Load Diff

View File

@ -62,28 +62,25 @@ class CBattleAI : public CBattleGameInterface
bool wasWaitingForRealize;
bool wasUnlockingGs;
int movesSkippedByDefense;
bool skipCastUntilNextBattle;
public:
CBattleAI();
~CBattleAI();
void initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB) override;
void attemptCastingSpell();
void initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB, AutocombatPreferences autocombatPreferences) override;
void evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps); //for offensive damaging spells only
void activeStack(const BattleID & battleID, const CStack * stack) override; //called when it's turn of that stack
void yourTacticPhase(const BattleID & battleID, int distance) override;
void activeStack(const CStack * stack) override; //called when it's turn of that stack
void yourTacticPhase(int distance) override;
std::optional<BattleAction> considerFleeingOrSurrendering();
std::optional<BattleAction> considerFleeingOrSurrendering(const BattleID & battleID);
void print(const std::string &text) const;
BattleAction useCatapult(const CStack *stack);
BattleAction useHealingTent(const CStack *stack);
BattleAction selectStackAction(const CStack * stack);
std::optional<PossibleSpellcast> findBestCreatureSpell(const CStack *stack);
BattleAction useCatapult(const BattleID & battleID, const CStack *stack);
BattleAction useHealingTent(const BattleID & battleID, const CStack *stack);
void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool Side, bool replayAllowed) override;
void battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool Side, bool replayAllowed) override;
//void actionFinished(const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero
//void actionStarted(const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero
//void battleAttack(const BattleAttack *ba) override; //called when stack is performing attack
@ -98,8 +95,5 @@ public:
//void battleTriggerEffect(const BattleTriggerEffect & bte) override;
//void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) override; //called by engine when battle starts; side=0 - left, side=1 - right
//void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack
private:
BattleAction goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes) const;
std::vector<BattleHex> getBrokenWallMoatHexes() const;
AutocombatPreferences autobattlePreferences = AutocombatPreferences();
};

View File

@ -0,0 +1,710 @@
/*
* BattleAI.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "BattleEvaluator.h"
#include "BattleExchangeVariant.h"
#include "StackWithBonuses.h"
#include "EnemyInfo.h"
#include "tbb/parallel_for.h"
#include "../../lib/CStopWatch.h"
#include "../../lib/CThreadHelper.h"
#include "../../lib/mapObjects/CGTownInstance.h"
#include "../../lib/spells/CSpellHandler.h"
#include "../../lib/spells/ISpellMechanics.h"
#include "../../lib/battle/BattleStateInfoForRetreat.h"
#include "../../lib/battle/CObstacleInstance.h"
#include "../../lib/battle/BattleAction.h"
// TODO: remove
// Eventually only IBattleInfoCallback and battle::Unit should be used,
// CUnitState should be private and CStack should be removed completely
#include "../../lib/CStack.h"
#define LOGL(text) print(text)
#define LOGFL(text, formattingEl) print(boost::str(boost::format(text) % formattingEl))
enum class SpellTypes
{
ADVENTURE, BATTLE, OTHER
};
SpellTypes spellType(const CSpell * spell)
{
if(!spell->isCombat() || spell->isCreatureAbility())
return SpellTypes::OTHER;
if(spell->isOffensive() || spell->hasEffects() || spell->hasBattleEffects())
return SpellTypes::BATTLE;
return SpellTypes::OTHER;
}
std::vector<BattleHex> BattleEvaluator::getBrokenWallMoatHexes() const
{
std::vector<BattleHex> result;
for(EWallPart wallPart : { EWallPart::BOTTOM_WALL, EWallPart::BELOW_GATE, EWallPart::OVER_GATE, EWallPart::UPPER_WALL })
{
auto state = cb->getBattle(battleID)->battleGetWallState(wallPart);
if(state != EWallState::DESTROYED)
continue;
auto wallHex = cb->getBattle(battleID)->wallPartToBattleHex((EWallPart)wallPart);
auto moatHex = wallHex.cloneInDirection(BattleHex::LEFT);
result.push_back(moatHex);
}
return result;
}
std::optional<PossibleSpellcast> BattleEvaluator::findBestCreatureSpell(const CStack *stack)
{
//TODO: faerie dragon type spell should be selected by server
SpellID creatureSpellToCast = cb->getBattle(battleID)->battleGetRandomStackSpell(CRandomGenerator::getDefault(), stack, CBattleInfoCallback::RANDOM_AIMED);
if(stack->hasBonusOfType(BonusType::SPELLCASTER) && stack->canCast() && creatureSpellToCast != SpellID::NONE)
{
const CSpell * spell = creatureSpellToCast.toSpell();
if(spell->canBeCast(cb->getBattle(battleID).get(), spells::Mode::CREATURE_ACTIVE, stack))
{
std::vector<PossibleSpellcast> possibleCasts;
spells::BattleCast temp(cb->getBattle(battleID).get(), stack, spells::Mode::CREATURE_ACTIVE, spell);
for(auto & target : temp.findPotentialTargets())
{
PossibleSpellcast ps;
ps.dest = target;
ps.spell = spell;
evaluateCreatureSpellcast(stack, ps);
possibleCasts.push_back(ps);
}
std::sort(possibleCasts.begin(), possibleCasts.end(), [&](const PossibleSpellcast & lhs, const PossibleSpellcast & rhs) { return lhs.value > rhs.value; });
if(!possibleCasts.empty() && possibleCasts.front().value > 0)
{
return possibleCasts.front();
}
}
}
return std::nullopt;
}
BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
{
#if BATTLE_TRACE_LEVEL >= 1
logAi->trace("Select stack action");
#endif
//evaluate casting spell for spellcasting stack
std::optional<PossibleSpellcast> bestSpellcast = findBestCreatureSpell(stack);
auto moveTarget = scoreEvaluator.findMoveTowardsUnreachable(stack, *targets, damageCache, hb);
float score = EvaluationResult::INEFFECTIVE_SCORE;
if(targets->possibleAttacks.empty() && bestSpellcast.has_value())
{
activeActionMade = true;
return BattleAction::makeCreatureSpellcast(stack, bestSpellcast->dest, bestSpellcast->spell->id);
}
if(!targets->possibleAttacks.empty())
{
#if BATTLE_TRACE_LEVEL>=1
logAi->trace("Evaluating attack for %s", stack->getDescription());
#endif
auto evaluationResult = scoreEvaluator.findBestTarget(stack, *targets, damageCache, hb);
auto & bestAttack = evaluationResult.bestAttack;
cachedAttack = bestAttack;
cachedScore = evaluationResult.score;
//TODO: consider more complex spellcast evaluation, f.e. because "re-retaliation" during enemy move in same turn for melee attack etc.
if(bestSpellcast.has_value() && bestSpellcast->value > bestAttack.damageDiff())
{
// return because spellcast value is damage dealt and score is dps reduce
activeActionMade = true;
return BattleAction::makeCreatureSpellcast(stack, bestSpellcast->dest, bestSpellcast->spell->id);
}
if(evaluationResult.score > score)
{
score = evaluationResult.score;
logAi->debug("BattleAI: %s -> %s x %d, from %d curpos %d dist %d speed %d: +%2f -%2f = %2f",
bestAttack.attackerState->unitType()->getJsonKey(),
bestAttack.affectedUnits[0]->unitType()->getJsonKey(),
(int)bestAttack.affectedUnits[0]->getCount(),
(int)bestAttack.from,
(int)bestAttack.attack.attacker->getPosition().hex,
bestAttack.attack.chargeDistance,
bestAttack.attack.attacker->speed(0, true),
bestAttack.defenderDamageReduce,
bestAttack.attackerDamageReduce,
bestAttack.attackValue()
);
if (moveTarget.scorePerTurn <= score)
{
if(evaluationResult.wait)
{
return BattleAction::makeWait(stack);
}
else if(bestAttack.attack.shooting)
{
activeActionMade = true;
return BattleAction::makeShotAttack(stack, bestAttack.attack.defender);
}
else
{
if(bestAttack.collateralDamageReduce
&& bestAttack.collateralDamageReduce >= bestAttack.defenderDamageReduce / 2
&& score < 0)
{
return BattleAction::makeDefend(stack);
}
else
{
activeActionMade = true;
return BattleAction::makeMeleeAttack(stack, bestAttack.attack.defender->getPosition(), bestAttack.from);
}
}
}
}
}
//ThreatMap threatsToUs(stack); // These lines may be usefull but they are't used in the code.
if(moveTarget.scorePerTurn > score)
{
score = moveTarget.score;
cachedAttack = moveTarget.cachedAttack;
cachedScore = score;
if(stack->waited())
{
return goTowardsNearest(stack, moveTarget.positions);
}
else
{
return BattleAction::makeWait(stack);
}
}
if(score <= EvaluationResult::INEFFECTIVE_SCORE
&& !stack->hasBonusOfType(BonusType::FLYING)
&& stack->unitSide() == BattleSide::ATTACKER
&& cb->getBattle(battleID)->battleGetSiegeLevel() >= CGTownInstance::CITADEL)
{
auto brokenWallMoat = getBrokenWallMoatHexes();
if(brokenWallMoat.size())
{
activeActionMade = true;
if(stack->doubleWide() && vstd::contains(brokenWallMoat, stack->getPosition()))
return BattleAction::makeMove(stack, stack->getPosition().cloneInDirection(BattleHex::RIGHT));
else
return goTowardsNearest(stack, brokenWallMoat);
}
}
return BattleAction::makeDefend(stack);
}
uint64_t timeElapsed(std::chrono::time_point<std::chrono::high_resolution_clock> start)
{
auto end = std::chrono::high_resolution_clock::now();
return std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
}
BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes)
{
auto reachability = cb->getBattle(battleID)->getReachability(stack);
auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false);
if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked
{
return BattleAction::makeDefend(stack);
}
std::sort(hexes.begin(), hexes.end(), [&](BattleHex h1, BattleHex h2) -> bool
{
return reachability.distances[h1] < reachability.distances[h2];
});
for(auto hex : hexes)
{
if(vstd::contains(avHexes, hex))
{
return BattleAction::makeMove(stack, hex);
}
if(stack->coversPos(hex))
{
logAi->warn("Warning: already standing on neighbouring tile!");
//We shouldn't even be here...
return BattleAction::makeDefend(stack);
}
}
BattleHex bestNeighbor = hexes.front();
if(reachability.distances[bestNeighbor] > GameConstants::BFIELD_SIZE)
{
return BattleAction::makeDefend(stack);
}
scoreEvaluator.updateReachabilityMap(hb);
if(stack->hasBonusOfType(BonusType::FLYING))
{
std::set<BattleHex> obstacleHexes;
auto insertAffected = [](const CObstacleInstance & spellObst, std::set<BattleHex> obstacleHexes) {
auto affectedHexes = spellObst.getAffectedTiles();
obstacleHexes.insert(affectedHexes.cbegin(), affectedHexes.cend());
};
const auto & obstacles = hb->battleGetAllObstacles();
for (const auto & obst: obstacles) {
if(obst->triggersEffects())
{
auto triggerAbility = VLC->spells()->getById(obst->getTrigger());
auto triggerIsNegative = triggerAbility->isNegative() || triggerAbility->isDamage();
if(triggerIsNegative)
insertAffected(*obst, obstacleHexes);
}
}
// Flying stack doesn't go hex by hex, so we can't backtrack using predecessors.
// We just check all available hexes and pick the one closest to the target.
auto nearestAvailableHex = vstd::minElementByFun(avHexes, [&](BattleHex hex) -> int
{
const int NEGATIVE_OBSTACLE_PENALTY = 100; // avoid landing on negative obstacle (moat, fire wall, etc)
const int BLOCKED_STACK_PENALTY = 100; // avoid landing on moat
auto distance = BattleHex::getDistance(bestNeighbor, hex);
if(vstd::contains(obstacleHexes, hex))
distance += NEGATIVE_OBSTACLE_PENALTY;
return scoreEvaluator.checkPositionBlocksOurStacks(*hb, stack, hex) ? BLOCKED_STACK_PENALTY + distance : distance;
});
return BattleAction::makeMove(stack, *nearestAvailableHex);
}
else
{
BattleHex currentDest = bestNeighbor;
while(1)
{
if(!currentDest.isValid())
{
return BattleAction::makeDefend(stack);
}
if(vstd::contains(avHexes, currentDest)
&& !scoreEvaluator.checkPositionBlocksOurStacks(*hb, stack, currentDest))
return BattleAction::makeMove(stack, currentDest);
currentDest = reachability.predecessors[currentDest];
}
}
}
bool BattleEvaluator::canCastSpell()
{
auto hero = cb->getBattle(battleID)->battleGetMyHero();
if(!hero)
return false;
return cb->getBattle(battleID)->battleCanCastSpell(hero, spells::Mode::HERO) == ESpellCastProblem::OK;
}
bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
{
auto hero = cb->getBattle(battleID)->battleGetMyHero();
if(!hero)
return false;
LOGL("Casting spells sounds like fun. Let's see...");
//Get all spells we can cast
std::vector<const CSpell*> possibleSpells;
vstd::copy_if(VLC->spellh->objects, std::back_inserter(possibleSpells), [hero, this](const CSpell *s) -> bool
{
return s->canBeCast(cb->getBattle(battleID).get(), spells::Mode::HERO, hero);
});
LOGFL("I can cast %d spells.", possibleSpells.size());
vstd::erase_if(possibleSpells, [](const CSpell *s)
{
return spellType(s) != SpellTypes::BATTLE || s->getTargetType() == spells::AimType::LOCATION;
});
LOGFL("I know how %d of them works.", possibleSpells.size());
//Get possible spell-target pairs
std::vector<PossibleSpellcast> possibleCasts;
for(auto spell : possibleSpells)
{
spells::BattleCast temp(cb->getBattle(battleID).get(), hero, spells::Mode::HERO, spell);
if(spell->getTargetType() == spells::AimType::LOCATION)
continue;
const bool FAST = true;
for(auto & target : temp.findPotentialTargets(FAST))
{
PossibleSpellcast ps;
ps.dest = target;
ps.spell = spell;
possibleCasts.push_back(ps);
}
}
LOGFL("Found %d spell-target combinations.", possibleCasts.size());
if(possibleCasts.empty())
return false;
using ValueMap = PossibleSpellcast::ValueMap;
auto evaluateQueue = [&](ValueMap & values, const std::vector<battle::Units> & queue, std::shared_ptr<HypotheticBattle> state, size_t minTurnSpan, bool * enemyHadTurnOut) -> bool
{
bool firstRound = true;
bool enemyHadTurn = false;
size_t ourTurnSpan = 0;
bool stop = false;
for(auto & round : queue)
{
if(!firstRound)
state->nextRound();
for(auto unit : round)
{
if(!vstd::contains(values, unit->unitId()))
values[unit->unitId()] = 0;
if(!unit->alive())
continue;
if(state->battleGetOwner(unit) != playerID)
{
enemyHadTurn = true;
if(!firstRound || state->battleCastSpells(unit->unitSide()) == 0)
{
//enemy could counter our spell at this point
//anyway, we do not know what enemy will do
//just stop evaluation
stop = true;
break;
}
}
else if(!enemyHadTurn)
{
ourTurnSpan++;
}
state->nextTurn(unit->unitId());
PotentialTargets pt(unit, damageCache, state);
if(!pt.possibleAttacks.empty())
{
AttackPossibility ap = pt.bestAction();
auto swb = state->getForUpdate(unit->unitId());
*swb = *ap.attackerState;
if(ap.defenderDamageReduce > 0)
swb->removeUnitBonus(Bonus::UntilAttack);
if(ap.attackerDamageReduce > 0)
swb->removeUnitBonus(Bonus::UntilBeingAttacked);
for(auto affected : ap.affectedUnits)
{
swb = state->getForUpdate(affected->unitId());
*swb = *affected;
if(ap.defenderDamageReduce > 0)
swb->removeUnitBonus(Bonus::UntilBeingAttacked);
if(ap.attackerDamageReduce > 0 && ap.attack.defender->unitId() == affected->unitId())
swb->removeUnitBonus(Bonus::UntilAttack);
}
}
auto bav = pt.bestActionValue();
//best action is from effective owner`s point if view, we need to convert to our point if view
if(state->battleGetOwner(unit) != playerID)
bav = -bav;
values[unit->unitId()] += bav;
}
firstRound = false;
if(stop)
break;
}
if(enemyHadTurnOut)
*enemyHadTurnOut = enemyHadTurn;
return ourTurnSpan >= minTurnSpan;
};
ValueMap valueOfStack;
ValueMap healthOfStack;
TStacks all = cb->getBattle(battleID)->battleGetAllStacks(false);
size_t ourRemainingTurns = 0;
for(auto unit : all)
{
healthOfStack[unit->unitId()] = unit->getAvailableHealth();
valueOfStack[unit->unitId()] = 0;
if(cb->getBattle(battleID)->battleGetOwner(unit) == playerID && unit->canMove() && !unit->moved())
ourRemainingTurns++;
}
LOGFL("I have %d turns left in this round", ourRemainingTurns);
const bool castNow = ourRemainingTurns <= 1;
if(castNow)
print("I should try to cast a spell now");
else
print("I could wait better moment to cast a spell");
auto amount = all.size();
std::vector<battle::Units> turnOrder;
cb->getBattle(battleID)->battleGetTurnOrder(turnOrder, amount, 2); //no more than 1 turn after current, each unit at least once
{
bool enemyHadTurn = false;
auto state = std::make_shared<HypotheticBattle>(env.get(), cb->getBattle(battleID));
evaluateQueue(valueOfStack, turnOrder, state, 0, &enemyHadTurn);
if(!enemyHadTurn)
{
auto battleIsFinishedOpt = state->battleIsFinished();
if(battleIsFinishedOpt)
{
print("No need to cast a spell. Battle will finish soon.");
return false;
}
}
}
CStopWatch timer;
#if BATTLE_TRACE_LEVEL >= 1
tbb::blocked_range<size_t> r(0, possibleCasts.size());
#else
tbb::parallel_for(tbb::blocked_range<size_t>(0, possibleCasts.size()), [&](const tbb::blocked_range<size_t> & r)
{
#endif
for(auto i = r.begin(); i != r.end(); i++)
{
auto & ps = possibleCasts[i];
#if BATTLE_TRACE_LEVEL >= 1
logAi->trace("Evaluating %s", ps.spell->getNameTranslated());
#endif
auto state = std::make_shared<HypotheticBattle>(env.get(), cb->getBattle(battleID));
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 needFullEval = vstd::contains_if(allUnits, [&](const battle::Unit * u) -> bool
{
auto original = cb->getBattle(battleID)->battleGetUnitByID(u->unitId());
return !original || u->speed() != original->speed();
});
DamageCache safeCopy = damageCache;
DamageCache innerCache(&safeCopy);
innerCache.buildDamageCache(state, side);
if(needFullEval || !cachedAttack)
{
#if BATTLE_TRACE_LEVEL >= 1
logAi->trace("Full evaluation is started due to stack speed affected.");
#endif
PotentialTargets innerTargets(activeStack, innerCache, state);
BattleExchangeEvaluator innerEvaluator(state, env, strengthRatio);
if(!innerTargets.possibleAttacks.empty())
{
innerEvaluator.updateReachabilityMap(state);
auto newStackAction = innerEvaluator.findBestTarget(activeStack, innerTargets, innerCache, state);
ps.value = newStackAction.score;
}
else
{
ps.value = 0;
}
}
else
{
ps.value = scoreEvaluator.calculateExchange(*cachedAttack, *targets, innerCache, state);
}
for(auto unit : allUnits)
{
auto newHealth = unit->getAvailableHealth();
auto oldHealth = healthOfStack[unit->unitId()];
if(oldHealth != newHealth)
{
auto damage = std::abs(oldHealth - newHealth);
auto originalDefender = cb->getBattle(battleID)->battleGetUnitByID(unit->unitId());
auto dpsReduce = AttackPossibility::calculateDamageReduce(
nullptr,
originalDefender && originalDefender->alive() ? originalDefender : unit,
damage,
innerCache,
state);
auto ourUnit = unit->unitSide() == side ? 1 : -1;
auto goodEffect = newHealth > oldHealth ? 1 : -1;
if(ourUnit * goodEffect == 1)
{
if(ourUnit && goodEffect && (unit->isClone() || unit->isGhost() || !unit->unitSlot().validSlot()))
continue;
ps.value += dpsReduce * scoreEvaluator.getPositiveEffectMultiplier();
}
else
ps.value -= dpsReduce * scoreEvaluator.getNegativeEffectMultiplier();
#if BATTLE_TRACE_LEVEL >= 1
logAi->trace(
"Spell affects %s (%d), dps: %2f",
unit->creatureId().toCreature()->getNameSingularTranslated(),
unit->getCount(),
dpsReduce);
#endif
}
}
#if BATTLE_TRACE_LEVEL >= 1
logAi->trace("Total score: %2f", ps.value);
#endif
}
#if BATTLE_TRACE_LEVEL == 0
});
#endif
LOGFL("Evaluation took %d ms", timer.getDiff());
auto pscValue = [](const PossibleSpellcast &ps) -> float
{
return ps.value;
};
auto castToPerform = *vstd::maxElementByFun(possibleCasts, pscValue);
if(castToPerform.value > cachedScore)
{
LOGFL("Best spell is %s (value %d). Will cast.", castToPerform.spell->getNameTranslated() % castToPerform.value);
BattleAction spellcast;
spellcast.actionType = EActionType::HERO_SPELL;
spellcast.spell = castToPerform.spell->id;
spellcast.setTarget(castToPerform.dest);
spellcast.side = side;
spellcast.stackNumber = (!side) ? -1 : -2;
cb->battleMakeSpellAction(battleID, spellcast);
activeActionMade = true;
return true;
}
LOGFL("Best spell is %s. But it is actually useless (value %d).", castToPerform.spell->getNameTranslated() % castToPerform.value);
return false;
}
//Below method works only for offensive spells
void BattleEvaluator::evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps)
{
using ValueMap = PossibleSpellcast::ValueMap;
RNGStub rngStub;
HypotheticBattle state(env.get(), cb->getBattle(battleID));
TStacks all = cb->getBattle(battleID)->battleGetAllStacks(false);
ValueMap healthOfStack;
ValueMap newHealthOfStack;
for(auto unit : all)
{
healthOfStack[unit->unitId()] = unit->getAvailableHealth();
}
spells::BattleCast cast(&state, stack, spells::Mode::CREATURE_ACTIVE, ps.spell);
cast.castEval(state.getServerCallback(), ps.dest);
for(auto unit : all)
{
auto unitId = unit->unitId();
auto localUnit = state.battleGetUnitByID(unitId);
newHealthOfStack[unitId] = localUnit->getAvailableHealth();
}
int64_t totalGain = 0;
for(auto unit : all)
{
auto unitId = unit->unitId();
auto localUnit = state.battleGetUnitByID(unitId);
auto healthDiff = newHealthOfStack[unitId] - healthOfStack[unitId];
if(localUnit->unitOwner() != cb->getBattle(battleID)->getPlayerID())
healthDiff = -healthDiff;
if(healthDiff < 0)
{
ps.value = -1;
return; //do not damage own units at all
}
totalGain += healthDiff;
}
ps.value = totalGain;
}
void BattleEvaluator::print(const std::string & text) const
{
logAi->trace("%s Battle AI[%p]: %s", playerID.toString(), this, text);
}

View File

@ -0,0 +1,83 @@
/*
* BattleEvaluator.h, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#pragma once
#include "../../lib/AI_Base.h"
#include "../../lib/battle/ReachabilityInfo.h"
#include "PossibleSpellcast.h"
#include "PotentialTargets.h"
#include "BattleExchangeVariant.h"
VCMI_LIB_NAMESPACE_BEGIN
class CSpell;
VCMI_LIB_NAMESPACE_END
class EnemyInfo;
class BattleEvaluator
{
std::unique_ptr<PotentialTargets> targets;
std::shared_ptr<HypotheticBattle> hb;
BattleExchangeEvaluator scoreEvaluator;
std::shared_ptr<CBattleCallback> cb;
std::shared_ptr<Environment> env;
bool activeActionMade = false;
std::optional<AttackPossibility> cachedAttack;
PlayerColor playerID;
BattleID battleID;
int side;
float cachedScore;
DamageCache damageCache;
float strengthRatio;
public:
BattleAction selectStackAction(const CStack * stack);
bool attemptCastingSpell(const CStack * stack);
bool canCastSpell();
std::optional<PossibleSpellcast> findBestCreatureSpell(const CStack * stack);
BattleAction goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes);
std::vector<BattleHex> getBrokenWallMoatHexes() const;
void evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps); //for offensive damaging spells only
void print(const std::string & text) const;
BattleEvaluator(
std::shared_ptr<Environment> env,
std::shared_ptr<CBattleCallback> cb,
const battle::Unit * activeStack,
PlayerColor playerID,
BattleID battleID,
int side,
float strengthRatio)
:scoreEvaluator(cb->getBattle(battleID), env, strengthRatio), cachedAttack(), playerID(playerID), side(side), env(env), cb(cb), strengthRatio(strengthRatio), battleID(battleID)
{
hb = std::make_shared<HypotheticBattle>(env.get(), cb->getBattle(battleID));
damageCache.buildDamageCache(hb, side);
targets = std::make_unique<PotentialTargets>(activeStack, damageCache, hb);
cachedScore = EvaluationResult::INEFFECTIVE_SCORE;
}
BattleEvaluator(
std::shared_ptr<Environment> env,
std::shared_ptr<CBattleCallback> cb,
std::shared_ptr<HypotheticBattle> hb,
DamageCache & damageCache,
const battle::Unit * activeStack,
PlayerColor playerID,
BattleID battleID,
int side,
float strengthRatio)
:scoreEvaluator(cb->getBattle(battleID), env, strengthRatio), cachedAttack(), playerID(playerID), side(side), env(env), cb(cb), hb(hb), damageCache(damageCache), strengthRatio(strengthRatio), battleID(battleID)
{
targets = std::make_unique<PotentialTargets>(activeStack, damageCache, hb);
cachedScore = EvaluationResult::INEFFECTIVE_SCORE;
}
};

View File

@ -18,75 +18,135 @@ AttackerValue::AttackerValue()
}
MoveTarget::MoveTarget()
: positions()
: positions(), cachedAttack()
{
score = EvaluationResult::INEFFECTIVE_SCORE;
scorePerTurn = EvaluationResult::INEFFECTIVE_SCORE;
turnsToRich = 1;
}
int64_t BattleExchangeVariant::trackAttack(const AttackPossibility & ap, HypotheticBattle & state)
float BattleExchangeVariant::trackAttack(
const AttackPossibility & ap,
std::shared_ptr<HypotheticBattle> hb,
DamageCache & damageCache)
{
auto attacker = hb->getForUpdate(ap.attack.attacker->unitId());
const std::string cachingStringBlocksRetaliation = "type_BLOCKS_RETALIATION";
static const auto selectorBlocksRetaliation = Selector::type()(BonusType::BLOCKS_RETALIATION);
const bool counterAttacksBlocked = attacker->hasBonus(selectorBlocksRetaliation, cachingStringBlocksRetaliation);
float attackValue = 0;
auto affectedUnits = ap.affectedUnits;
affectedUnits.push_back(ap.attackerState);
for(auto affectedUnit : affectedUnits)
{
auto unitToUpdate = state.getForUpdate(affectedUnit->unitId());
auto unitToUpdate = hb->getForUpdate(affectedUnit->unitId());
unitToUpdate->health = affectedUnit->health;
unitToUpdate->shots = affectedUnit->shots;
unitToUpdate->counterAttacks = affectedUnit->counterAttacks;
unitToUpdate->movedThisRound = affectedUnit->movedThisRound;
}
if(unitToUpdate->unitSide() == attacker->unitSide())
{
if(unitToUpdate->unitId() == attacker->unitId())
{
auto defender = hb->getForUpdate(ap.attack.defender->unitId());
auto attackValue = ap.attackValue();
if(!defender->alive() || counterAttacksBlocked || ap.attack.shooting || !defender->ableToRetaliate())
continue;
dpsScore += attackValue;
auto retaliationDamage = damageCache.getDamage(defender.get(), unitToUpdate.get(), hb);
auto attackerDamageReduce = AttackPossibility::calculateDamageReduce(defender.get(), unitToUpdate.get(), retaliationDamage, damageCache, hb);
attackValue -= attackerDamageReduce;
dpsScore -= attackerDamageReduce * negativeEffectMultiplier;
attackerValue[unitToUpdate->unitId()].isRetalitated = true;
unitToUpdate->damage(retaliationDamage);
defender->afterAttack(false, true);
#if BATTLE_TRACE_LEVEL>=1
logAi->trace(
"%s -> %s, ap attack, %s, dps: %lld, score: %lld",
ap.attack.attacker->getDescription(),
ap.attack.defender->getDescription(),
ap.attack.shooting ? "shot" : "mellee",
ap.damageDealt,
attackValue);
logAi->trace(
"%s -> %s, ap retalitation, %s, dps: %2f, score: %2f",
defender->getDescription(),
unitToUpdate->getDescription(),
ap.attack.shooting ? "shot" : "mellee",
retaliationDamage,
attackerDamageReduce);
#endif
}
else
{
auto collateralDamage = damageCache.getDamage(attacker.get(), unitToUpdate.get(), hb);
auto collateralDamageReduce = AttackPossibility::calculateDamageReduce(attacker.get(), unitToUpdate.get(), collateralDamage, damageCache, hb);
attackValue -= collateralDamageReduce;
dpsScore -= collateralDamageReduce * negativeEffectMultiplier;
unitToUpdate->damage(collateralDamage);
#if BATTLE_TRACE_LEVEL>=1
logAi->trace(
"%s -> %s, ap collateral, %s, dps: %2f, score: %2f",
attacker->getDescription(),
unitToUpdate->getDescription(),
ap.attack.shooting ? "shot" : "mellee",
collateralDamage,
collateralDamageReduce);
#endif
}
}
else
{
int64_t attackDamage = damageCache.getDamage(attacker.get(), unitToUpdate.get(), hb);
float defenderDamageReduce = AttackPossibility::calculateDamageReduce(attacker.get(), unitToUpdate.get(), attackDamage, damageCache, hb);
attackValue += defenderDamageReduce;
dpsScore += defenderDamageReduce * positiveEffectMultiplier;
attackerValue[attacker->unitId()].value += defenderDamageReduce;
unitToUpdate->damage(attackDamage);
#if BATTLE_TRACE_LEVEL>=1
logAi->trace(
"%s -> %s, ap attack, %s, dps: %2f, score: %2f",
attacker->getDescription(),
unitToUpdate->getDescription(),
ap.attack.shooting ? "shot" : "mellee",
attackDamage,
defenderDamageReduce);
#endif
}
}
attackValue += ap.shootersBlockedDmg;
dpsScore += ap.shootersBlockedDmg * positiveEffectMultiplier;
attacker->afterAttack(ap.attack.shooting, false);
return attackValue;
}
int64_t BattleExchangeVariant::trackAttack(
float BattleExchangeVariant::trackAttack(
std::shared_ptr<StackWithBonuses> attacker,
std::shared_ptr<StackWithBonuses> defender,
bool shooting,
bool isOurAttack,
const CBattleInfoCallback & cb,
DamageCache & damageCache,
std::shared_ptr<HypotheticBattle> hb,
bool evaluateOnly)
{
const std::string cachingStringBlocksRetaliation = "type_BLOCKS_RETALIATION";
static const auto selectorBlocksRetaliation = Selector::type()(BonusType::BLOCKS_RETALIATION);
const bool counterAttacksBlocked = attacker->hasBonus(selectorBlocksRetaliation, cachingStringBlocksRetaliation);
DamageEstimation retaliation;
// FIXME: provide distance info for Jousting bonus
BattleAttackInfo bai(attacker.get(), defender.get(), 0, shooting);
if(shooting)
{
bai.attackerPos.setXY(8, 5);
}
auto attack = cb.battleEstimateDamage(bai, &retaliation);
int64_t attackDamage = (attack.damage.min + attack.damage.max) / 2;
int64_t defenderDamageReduce = AttackPossibility::calculateDamageReduce(attacker.get(), defender.get(), attackDamage, cb);
int64_t attackerDamageReduce = 0;
int64_t attackDamage = damageCache.getDamage(attacker.get(), defender.get(), hb);
float defenderDamageReduce = AttackPossibility::calculateDamageReduce(attacker.get(), defender.get(), attackDamage, damageCache, hb);
float attackerDamageReduce = 0;
if(!evaluateOnly)
{
#if BATTLE_TRACE_LEVEL>=1
logAi->trace(
"%s -> %s, normal attack, %s, dps: %lld, %lld",
"%s -> %s, normal attack, %s, dps: %lld, %2f",
attacker->getDescription(),
defender->getDescription(),
shooting ? "shot" : "mellee",
@ -96,49 +156,43 @@ int64_t BattleExchangeVariant::trackAttack(
if(isOurAttack)
{
dpsScore += defenderDamageReduce;
dpsScore += defenderDamageReduce * positiveEffectMultiplier;
attackerValue[attacker->unitId()].value += defenderDamageReduce;
}
else
dpsScore -= defenderDamageReduce;
dpsScore -= defenderDamageReduce * negativeEffectMultiplier;
defender->damage(attackDamage);
attacker->afterAttack(shooting, false);
}
if(defender->alive() && defender->ableToRetaliate() && !counterAttacksBlocked && !shooting)
if(!evaluateOnly && defender->alive() && defender->ableToRetaliate() && !counterAttacksBlocked && !shooting)
{
if(retaliation.damage.max != 0)
{
auto retaliationDamage = (retaliation.damage.min + retaliation.damage.max) / 2;
attackerDamageReduce = AttackPossibility::calculateDamageReduce(defender.get(), attacker.get(), retaliationDamage, cb);
auto retaliationDamage = damageCache.getDamage(defender.get(), attacker.get(), hb);
attackerDamageReduce = AttackPossibility::calculateDamageReduce(defender.get(), attacker.get(), retaliationDamage, damageCache, hb);
if(!evaluateOnly)
{
#if BATTLE_TRACE_LEVEL>=1
logAi->trace(
"%s -> %s, retaliation, dps: %lld, %lld",
defender->getDescription(),
attacker->getDescription(),
retaliationDamage,
attackerDamageReduce);
logAi->trace(
"%s -> %s, retaliation, dps: %lld, %2f",
defender->getDescription(),
attacker->getDescription(),
retaliationDamage,
attackerDamageReduce);
#endif
if(isOurAttack)
{
dpsScore -= attackerDamageReduce;
attackerValue[attacker->unitId()].isRetalitated = true;
}
else
{
dpsScore += attackerDamageReduce;
attackerValue[defender->unitId()].value += attackerDamageReduce;
}
attacker->damage(retaliationDamage);
defender->afterAttack(false, true);
}
if(isOurAttack)
{
dpsScore -= attackerDamageReduce * negativeEffectMultiplier;
attackerValue[attacker->unitId()].isRetalitated = true;
}
else
{
dpsScore += attackerDamageReduce * positiveEffectMultiplier;
attackerValue[defender->unitId()].value += attackerDamageReduce;
}
attacker->damage(retaliationDamage);
defender->afterAttack(false, true);
}
auto score = defenderDamageReduce - attackerDamageReduce;
@ -146,44 +200,37 @@ int64_t BattleExchangeVariant::trackAttack(
#if BATTLE_TRACE_LEVEL>=1
if(!score)
{
logAi->trace("Attack has zero score d:%lld a:%lld", defenderDamageReduce, attackerDamageReduce);
logAi->trace("Attack has zero score d:%2f a:%2f", defenderDamageReduce, attackerDamageReduce);
}
#endif
return score;
}
EvaluationResult BattleExchangeEvaluator::findBestTarget(const battle::Unit * activeStack, PotentialTargets & targets, HypotheticBattle & hb)
EvaluationResult BattleExchangeEvaluator::findBestTarget(
const battle::Unit * activeStack,
PotentialTargets & targets,
DamageCache & damageCache,
std::shared_ptr<HypotheticBattle> hb)
{
EvaluationResult result(targets.bestAction());
updateReachabilityMap(hb);
for(auto & ap : targets.possibleAttacks)
{
int64_t score = calculateExchange(ap, targets, hb);
if(score > result.score)
{
result.score = score;
result.bestAttack = ap;
}
}
if(!activeStack->waited())
{
#if BATTLE_TRACE_LEVEL>=1
logAi->trace("Evaluating waited attack for %s", activeStack->getDescription());
#endif
hb.getForUpdate(activeStack->unitId())->waiting = true;
hb.getForUpdate(activeStack->unitId())->waitedThisTurn = true;
auto hbWaited = std::make_shared<HypotheticBattle>(env.get(), hb);
updateReachabilityMap(hb);
hbWaited->getForUpdate(activeStack->unitId())->waiting = true;
hbWaited->getForUpdate(activeStack->unitId())->waitedThisTurn = true;
updateReachabilityMap(hbWaited);
for(auto & ap : targets.possibleAttacks)
{
int64_t score = calculateExchange(ap, targets, hb);
float score = calculateExchange(ap, targets, damageCache, hbWaited);
if(score > result.score)
{
@ -194,13 +241,35 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(const battle::Unit * ac
}
}
#if BATTLE_TRACE_LEVEL>=1
logAi->trace("Evaluating normal attack for %s", activeStack->getDescription());
#endif
updateReachabilityMap(hb);
for(auto & ap : targets.possibleAttacks)
{
float score = calculateExchange(ap, targets, damageCache, hb);
if(score >= result.score)
{
result.score = score;
result.bestAttack = ap;
result.wait = false;
}
}
return result;
}
MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(const battle::Unit * activeStack, PotentialTargets & targets, HypotheticBattle & hb)
MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
const battle::Unit * activeStack,
PotentialTargets & targets,
DamageCache & damageCache,
std::shared_ptr<HypotheticBattle> hb)
{
MoveTarget result;
BattleExchangeVariant ev;
BattleExchangeVariant ev(getPositiveEffectMultiplier(), getNegativeEffectMultiplier());
if(targets.unreachableEnemies.empty())
return result;
@ -237,16 +306,20 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(const battle::Uni
{
// FIXME: provide distance info for Jousting bonus
auto bai = BattleAttackInfo(activeStack, closestStack, 0, cb->battleCanShoot(activeStack));
auto attack = AttackPossibility::evaluate(bai, hex, hb);
auto attack = AttackPossibility::evaluate(bai, hex, damageCache, hb);
attack.shootersBlockedDmg = 0; // we do not want to count on it, it is not for sure
auto score = calculateExchange(attack, targets, hb) / turnsToRich;
auto score = calculateExchange(attack, targets, damageCache, hb);
auto scorePerTurn = score / turnsToRich;
if(result.score < score)
if(result.scorePerTurn < scorePerTurn)
{
result.scorePerTurn = scorePerTurn;
result.score = score;
result.positions = closestStack->getAttackableHexes(activeStack);
result.cachedAttack = attack;
result.turnsToRich = turnsToRich;
}
}
}
@ -287,7 +360,7 @@ std::vector<const battle::Unit *> BattleExchangeEvaluator::getAdjacentUnits(cons
std::vector<const battle::Unit *> BattleExchangeEvaluator::getExchangeUnits(
const AttackPossibility & ap,
PotentialTargets & targets,
HypotheticBattle & hb)
std::shared_ptr<HypotheticBattle> hb)
{
auto hexes = ap.attack.defender->getHexes();
@ -308,7 +381,7 @@ std::vector<const battle::Unit *> BattleExchangeEvaluator::getExchangeUnits(
{
for(auto adjacentUnit : getAdjacentUnits(unit))
{
auto unitWithBonuses = hb.battleGetUnitByID(adjacentUnit->unitId());
auto unitWithBonuses = hb->battleGetUnitByID(adjacentUnit->unitId());
if(vstd::contains(targets.unreachableEnemies, adjacentUnit)
&& !vstd::contains(allReachableUnits, unitWithBonuses))
@ -343,21 +416,27 @@ std::vector<const battle::Unit *> BattleExchangeEvaluator::getExchangeUnits(
}
}
vstd::erase_if(exchangeUnits, [&](const battle::Unit * u) -> bool
{
return !hb->battleGetUnitByID(u->unitId())->alive();
});
return exchangeUnits;
}
int64_t BattleExchangeEvaluator::calculateExchange(
float BattleExchangeEvaluator::calculateExchange(
const AttackPossibility & ap,
PotentialTargets & targets,
HypotheticBattle & hb)
DamageCache & damageCache,
std::shared_ptr<HypotheticBattle> hb)
{
#if BATTLE_TRACE_LEVEL>=1
logAi->trace("Battle exchange at %lld", ap.attack.shooting ? ap.dest : ap.from);
logAi->trace("Battle exchange at %d", ap.attack.shooting ? ap.dest.hex : ap.from.hex);
#endif
if(cb->battleGetMySide() == BattlePerspective::LEFT_SIDE
&& cb->battleGetGateState() == EGateState::BLOCKED
&& ap.attack.defender->coversPos(ESiegeHex::GATE_BRIDGE))
&& ap.attack.defender->coversPos(BattleHex::GATE_BRIDGE))
{
return EvaluationResult::INEFFECTIVE_SCORE;
}
@ -365,7 +444,8 @@ int64_t BattleExchangeEvaluator::calculateExchange(
std::vector<const battle::Unit *> ourStacks;
std::vector<const battle::Unit *> enemyStacks;
enemyStacks.push_back(ap.attack.defender);
if(hb->battleGetUnitByID(ap.attack.defender->unitId())->alive())
enemyStacks.push_back(ap.attack.defender);
std::vector<const battle::Unit *> exchangeUnits = getExchangeUnits(ap, targets, hb);
@ -374,8 +454,23 @@ int64_t BattleExchangeEvaluator::calculateExchange(
return 0;
}
HypotheticBattle exchangeBattle(env.get(), cb);
BattleExchangeVariant v;
auto exchangeBattle = std::make_shared<HypotheticBattle>(env.get(), hb);
BattleExchangeVariant v(getPositiveEffectMultiplier(), getNegativeEffectMultiplier());
for(auto unit : exchangeUnits)
{
if(unit->isTurret())
continue;
bool isOur = exchangeBattle->battleMatchOwner(ap.attack.attacker, unit, true);
auto & attackerQueue = isOur ? ourStacks : enemyStacks;
if(exchangeBattle->getForUpdate(unit->unitId())->alive() && !vstd::contains(attackerQueue, unit))
{
attackerQueue.push_back(unit);
}
}
auto melleeAttackers = ourStacks;
vstd::removeDuplicates(melleeAttackers);
@ -384,30 +479,15 @@ int64_t BattleExchangeEvaluator::calculateExchange(
return !cb->battleCanShoot(u);
});
for(auto unit : exchangeUnits)
{
if(unit->isTurret())
continue;
bool isOur = cb->battleMatchOwner(ap.attack.attacker, unit, true);
auto & attackerQueue = isOur ? ourStacks : enemyStacks;
if(!vstd::contains(attackerQueue, unit))
{
attackerQueue.push_back(unit);
}
}
bool canUseAp = true;
for(auto activeUnit : exchangeUnits)
{
bool isOur = cb->battleMatchOwner(ap.attack.attacker, activeUnit, true);
bool isOur = exchangeBattle->battleMatchOwner(ap.attack.attacker, activeUnit, true);
battle::Units & attackerQueue = isOur ? ourStacks : enemyStacks;
battle::Units & oppositeQueue = isOur ? enemyStacks : ourStacks;
auto attacker = exchangeBattle.getForUpdate(activeUnit->unitId());
auto attacker = exchangeBattle->getForUpdate(activeUnit->unitId());
if(!attacker->alive())
{
@ -420,21 +500,22 @@ int64_t BattleExchangeEvaluator::calculateExchange(
auto targetUnit = ap.attack.defender;
if(!isOur || !exchangeBattle.getForUpdate(targetUnit->unitId())->alive())
if(!isOur || !exchangeBattle->battleGetUnitByID(targetUnit->unitId())->alive())
{
auto estimateAttack = [&](const battle::Unit * u) -> int64_t
auto estimateAttack = [&](const battle::Unit * u) -> float
{
auto stackWithBonuses = exchangeBattle.getForUpdate(u->unitId());
auto stackWithBonuses = exchangeBattle->getForUpdate(u->unitId());
auto score = v.trackAttack(
attacker,
stackWithBonuses,
exchangeBattle.battleCanShoot(stackWithBonuses.get()),
exchangeBattle->battleCanShoot(stackWithBonuses.get()),
isOur,
*cb,
damageCache,
hb,
true);
#if BATTLE_TRACE_LEVEL>=1
logAi->trace("Best target selector %s->%s score = %lld", attacker->getDescription(), u->getDescription(), score);
logAi->trace("Best target selector %s->%s score = %2f", attacker->getDescription(), u->getDescription(), score);
#endif
return score;
@ -446,9 +527,12 @@ int64_t BattleExchangeEvaluator::calculateExchange(
}
else
{
auto reachable = exchangeBattle.battleGetUnitsIf([&](const battle::Unit * u) -> bool
auto reachable = exchangeBattle->battleGetUnitsIf([&](const battle::Unit * u) -> bool
{
if(!u->alive() || u->unitSide() == attacker->unitSide())
if(u->unitSide() == attacker->unitSide())
return false;
if(!exchangeBattle->getForUpdate(u->unitId())->alive())
return false;
return vstd::contains_if(reachabilityMap[u->getPosition()], [&](const battle::Unit * other) -> bool
@ -472,19 +556,20 @@ int64_t BattleExchangeEvaluator::calculateExchange(
}
}
auto defender = exchangeBattle.getForUpdate(targetUnit->unitId());
auto shooting = cb->battleCanShoot(attacker.get());
auto defender = exchangeBattle->getForUpdate(targetUnit->unitId());
auto shooting = exchangeBattle->battleCanShoot(attacker.get());
const int totalAttacks = attacker->getTotalAttacks(shooting);
if(canUseAp && activeUnit == ap.attack.attacker && targetUnit == ap.attack.defender)
if(canUseAp && activeUnit->unitId() == ap.attack.attacker->unitId()
&& targetUnit->unitId() == ap.attack.defender->unitId())
{
v.trackAttack(ap, exchangeBattle);
v.trackAttack(ap, exchangeBattle, damageCache);
}
else
{
for(int i = 0; i < totalAttacks; i++)
{
v.trackAttack(attacker, defender, shooting, isOur, exchangeBattle);
v.trackAttack(attacker, defender, shooting, isOur, damageCache, exchangeBattle);
if(!attacker->alive() || !defender->alive())
break;
@ -495,12 +580,12 @@ int64_t BattleExchangeEvaluator::calculateExchange(
vstd::erase_if(attackerQueue, [&](const battle::Unit * u) -> bool
{
return !exchangeBattle.getForUpdate(u->unitId())->alive();
return !exchangeBattle->battleGetUnitByID(u->unitId())->alive();
});
vstd::erase_if(oppositeQueue, [&](const battle::Unit * u) -> bool
{
return !exchangeBattle.getForUpdate(u->unitId())->alive();
return !exchangeBattle->battleGetUnitByID(u->unitId())->alive();
});
}
@ -509,7 +594,7 @@ int64_t BattleExchangeEvaluator::calculateExchange(
v.adjustPositions(melleeAttackers, ap, reachabilityMap);
#if BATTLE_TRACE_LEVEL>=1
logAi->trace("Exchange score: %lld", v.getScore());
logAi->trace("Exchange score: %2f", v.getScore());
#endif
return v.getScore();
@ -539,7 +624,7 @@ void BattleExchangeVariant::adjustPositions(
vstd::erase_if_present(hexes, ap.attack.attacker->occupiedHex(ap.attack.attackerPos));
}
int64_t notRealizedDamage = 0;
float notRealizedDamage = 0;
for(auto unit : attackers)
{
@ -555,7 +640,7 @@ void BattleExchangeVariant::adjustPositions(
continue;
}
auto desiredPosition = vstd::minElementByFun(hexes, [&](BattleHex h) -> int64_t
auto desiredPosition = vstd::minElementByFun(hexes, [&](BattleHex h) -> float
{
auto score = vstd::contains(reachabilityMap[h], unit)
? reachabilityMap[h].size()
@ -581,13 +666,13 @@ void BattleExchangeVariant::adjustPositions(
}
}
void BattleExchangeEvaluator::updateReachabilityMap(HypotheticBattle & hb)
void BattleExchangeEvaluator::updateReachabilityMap( std::shared_ptr<HypotheticBattle> hb)
{
const int TURN_DEPTH = 2;
turnOrder.clear();
hb.battleGetTurnOrder(turnOrder, std::numeric_limits<int>::max(), TURN_DEPTH);
hb->battleGetTurnOrder(turnOrder, std::numeric_limits<int>::max(), TURN_DEPTH);
reachabilityMap.clear();
for(int turn = 0; turn < turnOrder.size(); turn++)

View File

@ -16,7 +16,7 @@
struct AttackerValue
{
int64_t value;
float value;
bool isRetalitated;
BattleHex position;
@ -25,20 +25,23 @@ struct AttackerValue
struct MoveTarget
{
int64_t score;
float score;
float scorePerTurn;
std::vector<BattleHex> positions;
std::optional<AttackPossibility> cachedAttack;
uint8_t turnsToRich;
MoveTarget();
};
struct EvaluationResult
{
static const int64_t INEFFECTIVE_SCORE = -1000000;
static const int64_t INEFFECTIVE_SCORE = -10000;
AttackPossibility bestAttack;
MoveTarget bestMove;
bool wait;
int64_t score;
float score;
bool defend;
EvaluationResult(const AttackPossibility & ap)
@ -56,19 +59,24 @@ struct EvaluationResult
class BattleExchangeVariant
{
public:
BattleExchangeVariant(): dpsScore(0) {}
BattleExchangeVariant(float positiveEffectMultiplier, float negativeEffectMultiplier)
: dpsScore(0), positiveEffectMultiplier(positiveEffectMultiplier), negativeEffectMultiplier(negativeEffectMultiplier) {}
int64_t trackAttack(const AttackPossibility & ap, HypotheticBattle & state);
float trackAttack(
const AttackPossibility & ap,
std::shared_ptr<HypotheticBattle> hb,
DamageCache & damageCache);
int64_t trackAttack(
float trackAttack(
std::shared_ptr<StackWithBonuses> attacker,
std::shared_ptr<StackWithBonuses> defender,
bool shooting,
bool isOurAttack,
const CBattleInfoCallback & cb,
DamageCache & damageCache,
std::shared_ptr<HypotheticBattle> hb,
bool evaluateOnly = false);
int64_t getScore() const { return dpsScore; }
float getScore() const { return dpsScore; }
void adjustPositions(
std::vector<const battle::Unit *> attackers,
@ -76,7 +84,9 @@ public:
std::map<BattleHex, battle::Units> & reachabilityMap);
private:
int64_t dpsScore;
float positiveEffectMultiplier;
float negativeEffectMultiplier;
float dpsScore;
std::map<uint32_t, AttackerValue> attackerValue;
};
@ -87,15 +97,40 @@ private:
std::shared_ptr<Environment> env;
std::map<BattleHex, std::vector<const battle::Unit *>> reachabilityMap;
std::vector<battle::Units> turnOrder;
float negativeEffectMultiplier;
public:
BattleExchangeEvaluator(std::shared_ptr<CBattleInfoCallback> cb, std::shared_ptr<Environment> env): cb(cb), env(env) {}
BattleExchangeEvaluator(
std::shared_ptr<CBattleInfoCallback> cb,
std::shared_ptr<Environment> env,
float strengthRatio): cb(cb), env(env) {
negativeEffectMultiplier = strengthRatio;
}
EvaluationResult findBestTarget(const battle::Unit * activeStack, PotentialTargets & targets, HypotheticBattle & hb);
int64_t calculateExchange(const AttackPossibility & ap, PotentialTargets & targets, HypotheticBattle & hb);
void updateReachabilityMap(HypotheticBattle & hb);
std::vector<const battle::Unit *> getExchangeUnits(const AttackPossibility & ap, PotentialTargets & targets, HypotheticBattle & hb);
EvaluationResult findBestTarget(
const battle::Unit * activeStack,
PotentialTargets & targets,
DamageCache & damageCache,
std::shared_ptr<HypotheticBattle> hb);
float calculateExchange(
const AttackPossibility & ap,
PotentialTargets & targets,
DamageCache & damageCache,
std::shared_ptr<HypotheticBattle> hb);
void updateReachabilityMap(std::shared_ptr<HypotheticBattle> hb);
std::vector<const battle::Unit *> getExchangeUnits(const AttackPossibility & ap, PotentialTargets & targets, std::shared_ptr<HypotheticBattle> hb);
bool checkPositionBlocksOurStacks(HypotheticBattle & hb, const battle::Unit * unit, BattleHex position);
MoveTarget findMoveTowardsUnreachable(const battle::Unit * activeStack, PotentialTargets & targets, HypotheticBattle & hb);
MoveTarget findMoveTowardsUnreachable(
const battle::Unit * activeStack,
PotentialTargets & targets,
DamageCache & damageCache,
std::shared_ptr<HypotheticBattle> hb);
std::vector<const battle::Unit *> getAdjacentUnits(const battle::Unit * unit);
};
float getPositiveEffectMultiplier() { return 1; }
float getNegativeEffectMultiplier() { return negativeEffectMultiplier; }
};

View File

@ -1,6 +1,7 @@
set(battleAI_SRCS
AttackPossibility.cpp
BattleAI.cpp
BattleEvaluator.cpp
common.cpp
EnemyInfo.cpp
PossibleSpellcast.cpp
@ -15,6 +16,7 @@ set(battleAI_HEADERS
AttackPossibility.h
BattleAI.h
BattleEvaluator.h
common.h
EnemyInfo.h
PotentialTargets.h
@ -37,7 +39,11 @@ else()
endif()
target_include_directories(BattleAI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(BattleAI PRIVATE ${VCMI_LIB_TARGET})
target_link_libraries(BattleAI PRIVATE ${VCMI_LIB_TARGET} TBB::tbb)
vcmi_set_output_dir(BattleAI "AI")
enable_pch(BattleAI)
if(APPLE_IOS AND NOT USING_CONAN)
install(IMPORTED_RUNTIME_ARTIFACTS TBB::tbb LIBRARY DESTINATION ${LIB_DIR}) # CMake 3.21+
endif()

View File

@ -27,7 +27,7 @@ public:
const CSpell * spell;
spells::Target dest;
int64_t value;
float value;
PossibleSpellcast();
virtual ~PossibleSpellcast();

View File

@ -11,11 +11,14 @@
#include "PotentialTargets.h"
#include "../../lib/CStack.h"//todo: remove
PotentialTargets::PotentialTargets(const battle::Unit * attacker, const HypotheticBattle & state)
PotentialTargets::PotentialTargets(
const battle::Unit * attacker,
DamageCache & damageCache,
std::shared_ptr<HypotheticBattle> state)
{
auto attackerInfo = state.battleGetUnitByID(attacker->unitId());
auto reachability = state.getReachability(attackerInfo);
auto avHexes = state.battleGetAvailableHexes(reachability, attackerInfo, false);
auto attackerInfo = state->battleGetUnitByID(attacker->unitId());
auto reachability = state->getReachability(attackerInfo);
auto avHexes = state->battleGetAvailableHexes(reachability, attackerInfo, false);
//FIXME: this should part of battleGetAvailableHexes
bool forceTarget = false;
@ -25,7 +28,7 @@ PotentialTargets::PotentialTargets(const battle::Unit * attacker, const Hypothet
if(attackerInfo->hasBonusOfType(BonusType::ATTACKS_NEAREST_CREATURE))
{
forceTarget = true;
auto nearest = state.getNearestStack(attackerInfo);
auto nearest = state->getNearestStack(attackerInfo);
if(nearest.first != nullptr)
{
@ -34,14 +37,14 @@ PotentialTargets::PotentialTargets(const battle::Unit * attacker, const Hypothet
}
}
auto aliveUnits = state.battleGetUnitsIf([=](const battle::Unit * unit)
auto aliveUnits = state->battleGetUnitsIf([=](const battle::Unit * unit)
{
return unit->isValidTarget() && unit->unitId() != attackerInfo->unitId();
});
for(auto defender : aliveUnits)
{
if(!forceTarget && !state.battleMatchOwner(attackerInfo, defender))
if(!forceTarget && !state->battleMatchOwner(attackerInfo, defender))
continue;
auto GenerateAttackInfo = [&](bool shooting, BattleHex hex) -> AttackPossibility
@ -49,7 +52,7 @@ PotentialTargets::PotentialTargets(const battle::Unit * attacker, const Hypothet
int distance = hex.isValid() ? reachability.distances[hex] : 0;
auto bai = BattleAttackInfo(attackerInfo, defender, distance, shooting);
return AttackPossibility::evaluate(bai, hex, state);
return AttackPossibility::evaluate(bai, hex, damageCache, state);
};
if(forceTarget)
@ -59,7 +62,7 @@ PotentialTargets::PotentialTargets(const battle::Unit * attacker, const Hypothet
else
unreachableEnemies.push_back(defender);
}
else if(state.battleCanShoot(attackerInfo, defender->getPosition()))
else if(state->battleCanShoot(attackerInfo, defender->getPosition()))
{
possibleAttacks.push_back(GenerateAttackInfo(true, BattleHex::INVALID));
}

View File

@ -17,7 +17,10 @@ public:
std::vector<const battle::Unit *> unreachableEnemies;
PotentialTargets(){};
PotentialTargets(const battle::Unit * attacker, const HypotheticBattle & state);
PotentialTargets(
const battle::Unit * attacker,
DamageCache & damageCache,
std::shared_ptr<HypotheticBattle> hb);
const AttackPossibility & bestAction() const;
int64_t bestActionValue() const;

View File

@ -45,13 +45,32 @@ StackWithBonuses::StackWithBonuses(const HypotheticBattle * Owner, const battle:
id(Stack->unitId()),
side(Stack->unitSide()),
player(Stack->unitOwner()),
slot(Stack->unitSlot())
slot(Stack->unitSlot()),
treeVersionLocal(0)
{
localInit(Owner);
battle::CUnitState::operator=(*Stack);
}
StackWithBonuses::StackWithBonuses(const HypotheticBattle * Owner, const battle::Unit * Stack)
: battle::CUnitState(),
origBearer(Stack->getBonusBearer()),
owner(Owner),
type(Stack->unitType()),
baseAmount(Stack->unitBaseAmount()),
id(Stack->unitId()),
side(Stack->unitSide()),
player(Stack->unitOwner()),
slot(Stack->unitSlot()),
treeVersionLocal(0)
{
localInit(Owner);
auto state = Stack->acquireState();
battle::CUnitState::operator=(*state);
}
StackWithBonuses::StackWithBonuses(const HypotheticBattle * Owner, const battle::UnitInfo & info)
: battle::CUnitState(),
origBearer(nullptr),
@ -59,7 +78,8 @@ StackWithBonuses::StackWithBonuses(const HypotheticBattle * Owner, const battle:
baseAmount(info.count),
id(info.id),
side(info.side),
slot(SlotID::SUMMONED_SLOT_PLACEHOLDER)
slot(SlotID::SUMMONED_SLOT_PLACEHOLDER),
treeVersionLocal(0)
{
type = info.type.toCreature();
origBearer = type;
@ -124,7 +144,7 @@ TConstBonusListPtr StackWithBonuses::getAllBonuses(const CSelector & selector, c
for(const Bonus & bonus : bonusesToUpdate)
{
if(selector(&bonus) && (!limit || !limit(&bonus)))
if(selector(&bonus) && (!limit || limit(&bonus)))
{
if(ret->getFirst(Selector::source(BonusSource::SPELL_EFFECT, bonus.sid).And(Selector::typeSubtype(bonus.type, bonus.subtype))))
{
@ -150,12 +170,18 @@ TConstBonusListPtr StackWithBonuses::getAllBonuses(const CSelector & selector, c
int64_t StackWithBonuses::getTreeVersion() const
{
return owner->getTreeVersion();
auto result = owner->getTreeVersion();
if(bonusesToAdd.empty() && bonusesToUpdate.empty() && bonusesToRemove.empty())
return result;
else
return result + treeVersionLocal;
}
void StackWithBonuses::addUnitBonus(const std::vector<Bonus> & bonus)
{
vstd::concatenate(bonusesToAdd, bonus);
treeVersionLocal++;
}
void StackWithBonuses::updateUnitBonus(const std::vector<Bonus> & bonus)
@ -163,6 +189,7 @@ void StackWithBonuses::updateUnitBonus(const std::vector<Bonus> & bonus)
//TODO: optimize, actualize to last value
vstd::concatenate(bonusesToUpdate, bonus);
treeVersionLocal++;
}
void StackWithBonuses::removeUnitBonus(const std::vector<Bonus> & bonus)
@ -197,12 +224,14 @@ void StackWithBonuses::removeUnitBonus(const CSelector & selector)
vstd::erase_if(bonusesToAdd, [&](const Bonus & b){return selector(&b);});
vstd::erase_if(bonusesToUpdate, [&](const Bonus & b){return selector(&b);});
treeVersionLocal++;
}
std::string StackWithBonuses::getDescription() const
{
std::ostringstream oss;
oss << unitOwner().getStr();
oss << unitOwner().toString();
oss << " battle stack [" << unitId() << "]: " << getCount() << " of ";
if(type)
oss << type->getJsonKey();
@ -256,7 +285,7 @@ std::shared_ptr<StackWithBonuses> HypotheticBattle::getForUpdate(uint32_t id)
if(iter == stackStates.end())
{
const CStack * s = subject->battleGetStackByID(id, false);
const battle::Unit * s = subject->battleGetUnitByID(id);
auto ret = std::make_shared<StackWithBonuses>(this, s);
stackStates[id] = ret;
@ -291,12 +320,17 @@ battle::Units HypotheticBattle::getUnitsIf(battle::UnitFilter predicate) const
return ret;
}
BattleID HypotheticBattle::getBattleID() const
{
return subject->getBattle()->getBattleID();
}
int32_t HypotheticBattle::getActiveStackID() const
{
return activeUnitId;
}
void HypotheticBattle::nextRound(int32_t roundNr)
void HypotheticBattle::nextRound()
{
//TODO:HypotheticBattle::nextRound
for(auto unit : battleAliveUnits())
@ -433,6 +467,24 @@ int64_t HypotheticBattle::getActualDamage(const DamageRange & damage, int32_t at
return (damage.min + damage.max) / 2;
}
std::vector<SpellID> HypotheticBattle::getUsedSpells(ui8 side) const
{
// TODO
return {};
}
int3 HypotheticBattle::getLocation() const
{
// TODO
return int3(-1, -1, -1);
}
bool HypotheticBattle::isCreatureBank() const
{
// TODO
return false;
}
int64_t HypotheticBattle::getTreeVersion() const
{
return getBonusBearer()->getTreeVersion() + bonusTreeVersion;
@ -523,8 +575,9 @@ const Services * HypotheticBattle::HypotheticEnvironment::services() const
return env->services();
}
const Environment::BattleCb * HypotheticBattle::HypotheticEnvironment::battle() const
const Environment::BattleCb * HypotheticBattle::HypotheticEnvironment::battle(const BattleID & battleID) const
{
assert(battleID == owner->getBattleID());
return owner;
}

View File

@ -47,9 +47,12 @@ public:
std::vector<Bonus> bonusesToAdd;
std::vector<Bonus> bonusesToUpdate;
std::set<std::shared_ptr<Bonus>> bonusesToRemove;
int treeVersionLocal;
StackWithBonuses(const HypotheticBattle * Owner, const battle::CUnitState * Stack);
StackWithBonuses(const HypotheticBattle * Owner, const battle::Unit * Stack);
StackWithBonuses(const HypotheticBattle * Owner, const battle::UnitInfo & info);
virtual ~StackWithBonuses();
@ -107,11 +110,13 @@ public:
std::shared_ptr<StackWithBonuses> getForUpdate(uint32_t id);
BattleID getBattleID() const override;
int32_t getActiveStackID() const override;
battle::Units getUnitsIf(battle::UnitFilter predicate) const override;
void nextRound(int32_t roundNr) override;
void nextRound() override;
void nextTurn(uint32_t unitId) override;
void addUnit(uint32_t id, const JsonNode & data) override;
@ -133,6 +138,9 @@ public:
uint32_t nextUnitId() const override;
int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const override;
std::vector<SpellID> getUsedSpells(ui8 side) const override;
int3 getLocation() const override;
bool isCreatureBank() const override;
int64_t getTreeVersion() const;
@ -174,7 +182,7 @@ private:
HypotheticEnvironment(HypotheticBattle * owner_, const Environment * upperEnvironment);
const Services * services() const override;
const BattleCb * battle() const override;
const BattleCb * battle(const BattleID & battleID) const override;
const GameCb * game() const override;
vstd::CLoggerBase * logger() const override;
events::EventBus * eventBus() const override;

View File

@ -1,11 +1,11 @@
/*
* StdInc.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
// Creates the precompiled header
#include "StdInc.h"
/*
* StdInc.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
// Creates the precompiled header
#include "StdInc.h"

View File

@ -1,17 +1,17 @@
/*
* StdInc.h, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#pragma once
#include "../../Global.h"
// This header should be treated as a pre compiled header file(PCH) in the compiler building settings.
// Here you can add specific libraries and macros which are specific to this project.
VCMI_LIB_USING_NAMESPACE
/*
* StdInc.h, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#pragma once
#include "../../Global.h"
// This header should be treated as a pre compiled header file(PCH) in the compiler building settings.
// Here you can add specific libraries and macros which are specific to this project.
VCMI_LIB_USING_NAMESPACE

View File

@ -1,33 +1,33 @@
/*
* main.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "../../lib/AI_Base.h"
#include "BattleAI.h"
#ifdef __GNUC__
#define strcpy_s(a, b, c) strncpy(a, c, b)
#endif
static const char *g_cszAiName = "Battle AI";
extern "C" DLL_EXPORT int GetGlobalAiVersion()
{
return AI_INTERFACE_VER;
}
extern "C" DLL_EXPORT void GetAiName(char* name)
{
strcpy_s(name, strlen(g_cszAiName) + 1, g_cszAiName);
}
extern "C" DLL_EXPORT void GetNewBattleAI(std::shared_ptr<CBattleGameInterface> &out)
{
out = std::make_shared<CBattleAI>();
}
/*
* main.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "../../lib/AI_Base.h"
#include "BattleAI.h"
#ifdef __GNUC__
#define strcpy_s(a, b, c) strncpy(a, c, b)
#endif
static const char *g_cszAiName = "Battle AI";
extern "C" DLL_EXPORT int GetGlobalAiVersion()
{
return AI_INTERFACE_VER;
}
extern "C" DLL_EXPORT void GetAiName(char* name)
{
strcpy_s(name, strlen(g_cszAiName) + 1, g_cszAiName);
}
extern "C" DLL_EXPORT void GetNewBattleAI(std::shared_ptr<CBattleGameInterface> &out)
{
out = std::make_shared<CBattleAI>();
}

View File

@ -1,75 +1,82 @@
/*
* CEmptyAI.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "CEmptyAI.h"
#include "../../lib/CRandomGenerator.h"
#include "../../lib/CStack.h"
void CEmptyAI::saveGame(BinarySerializer & h, const int version)
{
}
void CEmptyAI::loadGame(BinaryDeserializer & h, const int version)
{
}
void CEmptyAI::initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB)
{
cb = CB;
env = ENV;
human=false;
playerID = *cb->getMyColor();
}
void CEmptyAI::yourTurn()
{
cb->endTurn();
}
void CEmptyAI::activeStack(const CStack * stack)
{
cb->battleMakeUnitAction(BattleAction::makeDefend(stack));
}
void CEmptyAI::yourTacticPhase(int distance)
{
cb->battleMakeTacticAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide()));
}
void CEmptyAI::heroGotLevel(const CGHeroInstance *hero, PrimarySkill::PrimarySkill pskill, std::vector<SecondarySkill> &skills, QueryID queryID)
{
cb->selectionMade(CRandomGenerator::getDefault().nextInt((int)skills.size() - 1), queryID);
}
void CEmptyAI::commanderGotLevel(const CCommanderInstance * commander, std::vector<ui32> skills, QueryID queryID)
{
cb->selectionMade(CRandomGenerator::getDefault().nextInt((int)skills.size() - 1), queryID);
}
void CEmptyAI::showBlockingDialog(const std::string &text, const std::vector<Component> &components, QueryID askID, const int soundID, bool selection, bool cancel)
{
cb->selectionMade(0, askID);
}
void CEmptyAI::showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID)
{
cb->selectionMade(0, askID);
}
void CEmptyAI::showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID)
{
cb->selectionMade(0, queryID);
}
void CEmptyAI::showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector<ObjectInstanceID> & objects)
{
cb->selectionMade(0, askID);
}
/*
* CEmptyAI.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "CEmptyAI.h"
#include "../../lib/CRandomGenerator.h"
#include "../../lib/CStack.h"
#include "../../lib/battle/BattleAction.h"
void CEmptyAI::saveGame(BinarySerializer & h, const int version)
{
}
void CEmptyAI::loadGame(BinaryDeserializer & h, const int version)
{
}
void CEmptyAI::initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB)
{
cb = CB;
env = ENV;
human=false;
playerID = *cb->getPlayerID();
}
void CEmptyAI::yourTurn(QueryID queryID)
{
cb->selectionMade(0, queryID);
cb->endTurn();
}
void CEmptyAI::activeStack(const BattleID & battleID, const CStack * stack)
{
cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack));
}
void CEmptyAI::yourTacticPhase(const BattleID & battleID, int distance)
{
cb->battleMakeTacticAction(battleID, BattleAction::makeEndOFTacticPhase(cb->getBattle(battleID)->battleGetTacticsSide()));
}
void CEmptyAI::heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector<SecondarySkill> &skills, QueryID queryID)
{
cb->selectionMade(CRandomGenerator::getDefault().nextInt((int)skills.size() - 1), queryID);
}
void CEmptyAI::commanderGotLevel(const CCommanderInstance * commander, std::vector<ui32> skills, QueryID queryID)
{
cb->selectionMade(CRandomGenerator::getDefault().nextInt((int)skills.size() - 1), queryID);
}
void CEmptyAI::showBlockingDialog(const std::string &text, const std::vector<Component> &components, QueryID askID, const int soundID, bool selection, bool cancel)
{
cb->selectionMade(0, askID);
}
void CEmptyAI::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID)
{
cb->selectionMade(0, askID);
}
void CEmptyAI::showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID)
{
cb->selectionMade(0, queryID);
}
void CEmptyAI::showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector<ObjectInstanceID> & objects)
{
cb->selectionMade(0, askID);
}
std::optional<BattleAction> CEmptyAI::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState)
{
return std::nullopt;
}

View File

@ -1,37 +1,38 @@
/*
* CEmptyAI.h, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#pragma once
#include "../../lib/AI_Base.h"
#include "../../CCallback.h"
struct HeroMoveDetails;
class CEmptyAI : public CGlobalAI
{
std::shared_ptr<CCallback> cb;
public:
virtual void saveGame(BinarySerializer & h, const int version) override;
virtual void loadGame(BinaryDeserializer & h, const int version) override;
void initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB) override;
void yourTurn() override;
void yourTacticPhase(int distance) override;
void activeStack(const CStack * stack) override;
void heroGotLevel(const CGHeroInstance *hero, PrimarySkill::PrimarySkill pskill, std::vector<SecondarySkill> &skills, QueryID queryID) override;
void commanderGotLevel (const CCommanderInstance * commander, std::vector<ui32> skills, QueryID queryID) override;
void showBlockingDialog(const std::string &text, const std::vector<Component> &components, QueryID askID, const int soundID, bool selection, bool cancel) override;
void showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override;
void showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) override;
void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector<ObjectInstanceID> & objects) override;
};
#define NAME "EmptyAI 0.1"
/*
* CEmptyAI.h, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#pragma once
#include "../../lib/AI_Base.h"
#include "../../CCallback.h"
struct HeroMoveDetails;
class CEmptyAI : public CGlobalAI
{
std::shared_ptr<CCallback> cb;
public:
virtual void saveGame(BinarySerializer & h, const int version) override;
virtual void loadGame(BinaryDeserializer & h, const int version) override;
void initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB) override;
void yourTurn(QueryID queryID) override;
void yourTacticPhase(const BattleID & battleID, int distance) override;
void activeStack(const BattleID & battleID, const CStack * stack) override;
void heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector<SecondarySkill> &skills, QueryID queryID) override;
void commanderGotLevel (const CCommanderInstance * commander, std::vector<ui32> skills, QueryID queryID) override;
void showBlockingDialog(const std::string &text, const std::vector<Component> &components, QueryID askID, const int soundID, bool selection, bool cancel) override;
void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override;
void showGarrisonDialog(const CArmedInstance *up, const CGHeroInstance *down, bool removableUnits, QueryID queryID) override;
void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector<ObjectInstanceID> & objects) override;
std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override;
};
#define NAME "EmptyAI 0.1"

View File

@ -1,2 +1,2 @@
// Creates the precompiled header
// Creates the precompiled header
#include "StdInc.h"

View File

@ -1,9 +1,9 @@
#pragma once
#include "../../Global.h"
// This header should be treated as a pre compiled header file(PCH) in the compiler building settings.
// Here you can add specific libraries and macros which are specific to this project.
VCMI_LIB_USING_NAMESPACE
#pragma once
#include "../../Global.h"
// This header should be treated as a pre compiled header file(PCH) in the compiler building settings.
// Here you can add specific libraries and macros which are specific to this project.
VCMI_LIB_USING_NAMESPACE

View File

@ -1,28 +1,28 @@
/*
* main.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "CEmptyAI.h"
std::set<CGlobalAI*> ais;
extern "C" DLL_EXPORT int GetGlobalAiVersion()
{
return AI_INTERFACE_VER;
}
extern "C" DLL_EXPORT void GetAiName(char* name)
{
strcpy(name,NAME);
}
extern "C" DLL_EXPORT void GetNewAI(std::shared_ptr<CGlobalAI> &out)
{
out = std::make_shared<CEmptyAI>();
}
/*
* main.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "CEmptyAI.h"
std::set<CGlobalAI*> ais;
extern "C" DLL_EXPORT int GetGlobalAiVersion()
{
return AI_INTERFACE_VER;
}
extern "C" DLL_EXPORT void GetAiName(char* name)
{
strcpy(name,NAME);
}
extern "C" DLL_EXPORT void GetNewAI(std::shared_ptr<CGlobalAI> &out)
{
out = std::make_shared<CEmptyAI>();
}

File diff suppressed because it is too large Load Diff

View File

@ -21,6 +21,7 @@
#include "../../lib/serializer/BinarySerializer.h"
#include "../../lib/serializer/BinaryDeserializer.h"
#include "../../lib/battle/BattleStateInfoForRetreat.h"
#include "../../lib/battle/BattleInfo.h"
#include "AIGateway.h"
#include "Goals/Goals.h"
@ -34,26 +35,26 @@ 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
boost::thread_specific_ptr<CCallback> cb;
boost::thread_specific_ptr<AIGateway> ai;
thread_local CCallback * cb = nullptr;
thread_local AIGateway * ai = nullptr;
//helper RAII to manage global ai/cb ptrs
struct SetGlobalState
{
SetGlobalState(AIGateway * AI)
{
assert(!ai.get());
assert(!cb.get());
assert(!ai);
assert(!cb);
ai.reset(AI);
cb.reset(AI->myCb.get());
ai = AI;
cb = AI->myCb.get();
}
~SetGlobalState()
{
//TODO: how to handle rm? shouldn't be called after ai is destroyed, hopefully
//TODO: to ensure that, make rm unique_ptr
ai.release();
cb.release();
ai = nullptr;
cb = nullptr;
}
};
@ -153,10 +154,13 @@ void AIGateway::artifactAssembled(const ArtifactLocation & al)
NET_EVENT_HANDLER;
}
void AIGateway::showTavernWindow(const CGObjectInstance * townOrTavern)
void AIGateway::showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID)
{
LOG_TRACE(logAi);
NET_EVENT_HANDLER;
status.addQuery(queryID, "TavernWindow");
requestActionASAP([=](){ answerQuery(queryID, 0); });
}
void AIGateway::showThievesGuildWindow(const CGObjectInstance * obj)
@ -192,7 +196,7 @@ void AIGateway::gameOver(PlayerColor player, const EVictoryLossCheckResult & vic
{
LOG_TRACE_PARAMS(logAi, "victoryLossCheckResult '%s'", victoryLossCheckResult.messageToSelf.toString());
NET_EVENT_HANDLER;
logAi->debug("Player %d (%s): I heard that player %d (%s) %s.", playerID, playerID.getStr(), player, player.getStr(), (victoryLossCheckResult.victory() ? "won" : "lost"));
logAi->debug("Player %d (%s): I heard that player %d (%s) %s.", playerID, playerID.toString(), player, player.toString(), (victoryLossCheckResult.victory() ? "won" : "lost"));
// some whitespace to flush stream
logAi->debug(std::string(200, ' '));
@ -201,12 +205,12 @@ void AIGateway::gameOver(PlayerColor player, const EVictoryLossCheckResult & vic
{
if(victoryLossCheckResult.victory())
{
logAi->debug("AIGateway: Player %d (%s) won. I won! Incredible!", player, player.getStr());
logAi->debug("AIGateway: Player %d (%s) won. I won! Incredible!", player, player.toString());
logAi->debug("Turn nr %d", myCb->getDate());
}
else
{
logAi->debug("AIGateway: Player %d (%s) lost. It's me. What a disappointment! :(", player, player.getStr());
logAi->debug("AIGateway: Player %d (%s) lost. It's me. What a disappointment! :(", player, player.toString());
}
// some whitespace to flush stream
@ -314,16 +318,23 @@ void AIGateway::heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID her
});
}
void AIGateway::heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val)
void AIGateway::heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val)
{
LOG_TRACE_PARAMS(logAi, "which '%i', val '%i'", which % val);
LOG_TRACE_PARAMS(logAi, "which '%i', val '%i'", static_cast<int>(which) % val);
NET_EVENT_HANDLER;
}
void AIGateway::showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level)
void AIGateway::showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level, QueryID queryID)
{
LOG_TRACE_PARAMS(logAi, "level '%i'", level);
NET_EVENT_HANDLER;
status.addQuery(queryID, "RecruitmentDialog");
requestActionASAP([=](){
recruitCreatures(dwelling, dst);
answerQuery(queryID, 0);
});
}
void AIGateway::heroMovePointsChanged(const CGHeroInstance * hero)
@ -348,7 +359,7 @@ void AIGateway::newObject(const CGObjectInstance * obj)
//to prevent AI from accessing objects that got deleted while they became invisible (Cover of Darkness, enemy hero moved etc.) below code allows AI to know deletion of objects out of sight
//see: RemoveObject::applyFirstCl, to keep AI "not cheating" do not use advantage of this and use this function just to prevent crashes
void AIGateway::objectRemoved(const CGObjectInstance * obj)
void AIGateway::objectRemoved(const CGObjectInstance * obj, const PlayerColor & initiator)
{
LOG_TRACE(logAi);
NET_EVENT_HANDLER;
@ -387,7 +398,7 @@ void AIGateway::heroCreated(const CGHeroInstance * h)
NET_EVENT_HANDLER;
}
void AIGateway::advmapSpellCast(const CGHeroInstance * caster, int spellID)
void AIGateway::advmapSpellCast(const CGHeroInstance * caster, SpellID spellID)
{
LOG_TRACE_PARAMS(logAi, "spellID '%i", spellID);
NET_EVENT_HANDLER;
@ -424,10 +435,13 @@ void AIGateway::receivedResource()
NET_EVENT_HANDLER;
}
void AIGateway::showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor)
void AIGateway::showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID)
{
LOG_TRACE(logAi);
NET_EVENT_HANDLER;
status.addQuery(queryID, "UniversityWindow");
requestActionASAP([=](){ answerQuery(queryID, 0); });
}
void AIGateway::heroManaPointsChanged(const CGHeroInstance * hero)
@ -497,10 +511,13 @@ void AIGateway::heroBonusChanged(const CGHeroInstance * hero, const Bonus & bonu
NET_EVENT_HANDLER;
}
void AIGateway::showMarketWindow(const IMarket * market, const CGHeroInstance * visitor)
void AIGateway::showMarketWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID)
{
LOG_TRACE(logAi);
NET_EVENT_HANDLER;
status.addQuery(queryID, "MarketWindow");
requestActionASAP([=](){ answerQuery(queryID, 0); });
}
void AIGateway::showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain)
@ -510,7 +527,7 @@ void AIGateway::showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositio
NET_EVENT_HANDLER;
}
std::optional<BattleAction> AIGateway::makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState)
std::optional<BattleAction> AIGateway::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState)
{
LOG_TRACE(logAi);
NET_EVENT_HANDLER;
@ -535,7 +552,7 @@ void AIGateway::initGameInterface(std::shared_ptr<Environment> env, std::shared_
cbc = CB;
NET_EVENT_HANDLER;
playerID = *myCb->getMyColor();
playerID = *myCb->getPlayerID();
myCb->waitTillRealize = true;
myCb->unlockGsWhenWaiting = true;
@ -544,15 +561,17 @@ void AIGateway::initGameInterface(std::shared_ptr<Environment> env, std::shared_
retrieveVisitableObjs();
}
void AIGateway::yourTurn()
void AIGateway::yourTurn(QueryID queryID)
{
LOG_TRACE(logAi);
LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID);
NET_EVENT_HANDLER;
status.addQuery(queryID, "YourTurn");
requestActionASAP([=](){ answerQuery(queryID, 0); });
status.startedTurn();
makingTurn = std::make_unique<boost::thread>(&AIGateway::makeTurn, this);
}
void AIGateway::heroGotLevel(const CGHeroInstance * hero, PrimarySkill::PrimarySkill pskill, std::vector<SecondarySkill> & skills, QueryID queryID)
void AIGateway::heroGotLevel(const CGHeroInstance * hero, PrimarySkill pskill, std::vector<SecondarySkill> & skills, QueryID queryID)
{
LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID);
NET_EVENT_HANDLER;
@ -651,7 +670,7 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector<C
});
}
void AIGateway::showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID)
void AIGateway::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID)
{
NET_EVENT_HANDLER;
status.addQuery(askID, boost::str(boost::format("Teleport dialog query with %d exits") % exits.size()));
@ -699,8 +718,8 @@ void AIGateway::showGarrisonDialog(const CArmedInstance * up, const CGHeroInstan
LOG_TRACE_PARAMS(logAi, "removableUnits '%i', queryID '%i'", removableUnits % queryID);
NET_EVENT_HANDLER;
std::string s1 = up ? up->nodeName() : "NONE";
std::string s2 = down ? down->nodeName() : "NONE";
std::string s1 = up->nodeName();
std::string s2 = down->nodeName();
status.addQuery(queryID, boost::str(boost::format("Garrison dialog with %s and %s") % s1 % s2));
@ -708,7 +727,9 @@ void AIGateway::showGarrisonDialog(const CArmedInstance * up, const CGHeroInstan
requestActionASAP([=]()
{
if(removableUnits && up->tempOwner == down->tempOwner)
{
pickBestCreatures(down, up);
}
answerQuery(queryID, 0);
});
@ -757,7 +778,7 @@ bool AIGateway::makePossibleUpgrades(const CArmedInstance * obj)
{
UpgradeInfo ui;
myCb->fillUpgradeInfo(obj, SlotID(i), ui);
if(ui.oldID >= 0 && nullkiller->getFreeResources().canAfford(ui.cost[0] * s->count))
if(ui.oldID != CreatureID::NONE && nullkiller->getFreeResources().canAfford(ui.cost[0] * s->count))
{
myCb->upgradeCreature(obj, SlotID(i), ui.newID[0]);
upgraded = true;
@ -773,8 +794,8 @@ void AIGateway::makeTurn()
{
MAKING_TURN;
auto day = cb->getDate(Date::EDateType::DAY);
logAi->info("Player %d (%s) starting turn, day %d", playerID, playerID.getStr(), day);
auto day = cb->getDate(Date::DAY);
logAi->info("Player %d (%s) starting turn, day %d", playerID, playerID.toString(), day);
boost::shared_lock<boost::shared_mutex> gsLock(CGameState::mutex);
setThreadName("AIGateway::makeTurn");
@ -828,9 +849,6 @@ void AIGateway::performObjectInteraction(const CGObjectInstance * obj, HeroPtr h
LOG_TRACE_PARAMS(logAi, "Hero %s and object %s at %s", h->getNameTranslated() % obj->getObjectName() % obj->pos.toString());
switch(obj->ID)
{
case Obj::CREATURE_GENERATOR1:
recruitCreatures(dynamic_cast<const CGDwelling *>(obj), h.get());
break;
case Obj::TOWN:
if(h->visitedTown) //we are inside, not just attacking
{
@ -1078,26 +1096,26 @@ void AIGateway::recruitCreatures(const CGDwelling * d, const CArmedInstance * re
}
}
void AIGateway::battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed)
void AIGateway::battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed)
{
NET_EVENT_HANDLER;
assert(playerID > PlayerColor::PLAYER_LIMIT || status.getBattle() == UPCOMING_BATTLE);
assert(!playerID.isValidPlayer() || status.getBattle() == UPCOMING_BATTLE);
status.setBattle(ONGOING_BATTLE);
const CGObjectInstance * presumedEnemy = vstd::backOrNull(cb->getVisitableObjs(tile)); //may be nullptr in some very are cases -> eg. visited monolith and fighting with an enemy at the FoW covered exit
battlename = boost::str(boost::format("Starting battle of %s attacking %s at %s") % (hero1 ? hero1->getNameTranslated() : "a army") % (presumedEnemy ? presumedEnemy->getObjectName() : "unknown enemy") % tile.toString());
CAdventureAI::battleStart(army1, army2, tile, hero1, hero2, side, replayAllowed);
CAdventureAI::battleStart(battleID, army1, army2, tile, hero1, hero2, side, replayAllowed);
}
void AIGateway::battleEnd(const BattleResult * br, QueryID queryID)
void AIGateway::battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID)
{
NET_EVENT_HANDLER;
assert(status.getBattle() == ONGOING_BATTLE);
status.setBattle(ENDING_BATTLE);
bool won = br->winner == myCb->battleGetMySide();
logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.getStr(), (won ? "won" : "lost"), battlename);
bool won = br->winner == myCb->getBattle(battleID)->battleGetMySide();
logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.toString(), (won ? "won" : "lost"), battlename);
battlename.clear();
if (queryID != -1)
if (queryID != QueryID::NONE)
{
status.addQuery(queryID, "Combat result dialog");
const int confirmAction = 0;
@ -1106,7 +1124,7 @@ void AIGateway::battleEnd(const BattleResult * br, QueryID queryID)
answerQuery(queryID, confirmAction);
});
}
CAdventureAI::battleEnd(br, queryID);
CAdventureAI::battleEnd(battleID, br, queryID);
}
void AIGateway::waitTillFree()
@ -1419,7 +1437,7 @@ void AIGateway::tryRealize(Goals::Trade & g) //trade
void AIGateway::endTurn()
{
logAi->info("Player %d (%s) ends turn", playerID, playerID.getStr());
logAi->info("Player %d (%s) ends turn", playerID, playerID.toString());
if(!status.haveTurn())
{
logAi->error("Not having turn at the end of turn???");
@ -1439,7 +1457,7 @@ void AIGateway::endTurn()
}
while(status.haveTurn()); //for some reasons, our request may fail -> stop requesting end of turn only after we've received a confirmation that it's over
logGlobal->info("Player %d (%s) ended turn", playerID, playerID.getStr());
logGlobal->info("Player %d (%s) ended turn", playerID, playerID.toString());
}
void AIGateway::buildArmyIn(const CGTownInstance * t)
@ -1472,6 +1490,8 @@ void AIGateway::requestActionASAP(std::function<void()> whatToDo)
boost::shared_lock<boost::shared_mutex> gsLock(CGameState::mutex);
whatToDo();
});
newThread.detach();
}
void AIGateway::lostHero(HeroPtr h)
@ -1605,7 +1625,7 @@ void AIStatus::waitTillFree()
{
boost::unique_lock<boost::mutex> lock(mx);
while(battle != NO_BATTLE || !remainingQueries.empty() || !objectsBeingVisited.empty() || ongoingHeroMovement)
cv.timed_wait(lock, boost::posix_time::milliseconds(10));
cv.wait_for(lock, boost::chrono::milliseconds(10));
}
bool AIStatus::haveTurn()

View File

@ -111,13 +111,13 @@ public:
std::string getBattleAIName() const override;
void initGameInterface(std::shared_ptr<Environment> env, std::shared_ptr<CCallback> CB) override;
void yourTurn() override;
void yourTurn(QueryID queryID) override;
void heroGotLevel(const CGHeroInstance * hero, PrimarySkill::PrimarySkill pskill, std::vector<SecondarySkill> & skills, QueryID queryID) override; //pskill is gained primary skill, interface has to choose one of given skills and call callback with selection id
void heroGotLevel(const CGHeroInstance * hero, PrimarySkill pskill, std::vector<SecondarySkill> & skills, QueryID queryID) override; //pskill is gained primary skill, interface has to choose one of given skills and call callback with selection id
void commanderGotLevel(const CCommanderInstance * commander, std::vector<ui32> skills, QueryID queryID) override; //TODO
void showBlockingDialog(const std::string & text, const std::vector<Component> & components, QueryID askID, const int soundID, bool selection, bool cancel) override; //Show a dialog, player must take decision. If selection then he has to choose between one of given components, if cancel he is allowed to not choose. After making choice, CCallback::selectionMade should be called with number of selected component (1 - n) or 0 for cancel (if allowed) and askID.
void showGarrisonDialog(const CArmedInstance * up, const CGHeroInstance * down, bool removableUnits, QueryID queryID) override; //all stacks operations between these objects become allowed, interface has to call onEnd when done
void showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override;
void showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override;
void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector<ObjectInstanceID> & objects) override;
void saveGame(BinarySerializer & h, const int version) override; //saving
void loadGame(BinaryDeserializer & h, const int version) override; //loading
@ -130,7 +130,7 @@ public:
void tileHidden(const std::unordered_set<int3> & pos) override;
void artifactMoved(const ArtifactLocation & src, const ArtifactLocation & dst) override;
void artifactAssembled(const ArtifactLocation & al) override;
void showTavernWindow(const CGObjectInstance * townOrTavern) override;
void showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID) override;
void showThievesGuildWindow(const CGObjectInstance * obj) override;
void playerBlocked(int reason, bool start) override;
void showPuzzleMap() override;
@ -144,20 +144,20 @@ public:
void heroVisitsTown(const CGHeroInstance * hero, const CGTownInstance * town) override;
void tileRevealed(const std::unordered_set<int3> & pos) override;
void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) override;
void heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val) override;
void showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level) override;
void heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val) override;
void showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level, QueryID queryID) override;
void heroMovePointsChanged(const CGHeroInstance * hero) override;
void garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) override;
void newObject(const CGObjectInstance * obj) override;
void showHillFortWindow(const CGObjectInstance * object, const CGHeroInstance * visitor) override;
void playerBonusChanged(const Bonus & bonus, bool gain) override;
void heroCreated(const CGHeroInstance *) override;
void advmapSpellCast(const CGHeroInstance * caster, int spellID) override;
void advmapSpellCast(const CGHeroInstance * caster, SpellID spellID) override;
void showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector<Component> & components, int soundID) override;
void requestRealized(PackageApplied * pa) override;
void receivedResource() override;
void objectRemoved(const CGObjectInstance * obj) override;
void showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor) override;
void objectRemoved(const CGObjectInstance * obj, const PlayerColor & initiator) override;
void showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) override;
void heroManaPointsChanged(const CGHeroInstance * hero) override;
void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) override;
void battleResultsApplied() override;
@ -165,12 +165,12 @@ public:
void objectPropertyChanged(const SetObjectProperty * sop) override;
void buildChanged(const CGTownInstance * town, BuildingID buildingID, int what) override;
void heroBonusChanged(const CGHeroInstance * hero, const Bonus & bonus, bool gain) override;
void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor) override;
void showMarketWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) override;
void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override;
std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleStateInfoForRetreat & battleState) override;
std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) override;
void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) override;
void battleEnd(const BattleResult * br, QueryID queryID) override;
void battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool side, bool replayAllowed) override;
void battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID) override;
void makeTurn();

View File

@ -25,10 +25,6 @@
namespace NKAI
{
extern boost::thread_specific_ptr<AIGateway> ai;
//extern static const int3 dirs[8];
const CGObjectInstance * ObjectIdRef::operator->() const
{
return cb->getObj(id, false);
@ -245,7 +241,7 @@ bool isObjectPassable(const CGObjectInstance * obj)
}
// Pathfinder internal helper
bool isObjectPassable(const CGObjectInstance * obj, PlayerColor playerColor, PlayerRelations::PlayerRelations objectRelations)
bool isObjectPassable(const CGObjectInstance * obj, PlayerColor playerColor, PlayerRelations objectRelations)
{
if((obj->ID == Obj::GARRISON || obj->ID == Obj::GARRISON2)
&& objectRelations != PlayerRelations::ENEMIES)
@ -278,7 +274,7 @@ creInfo infoFromDC(const dwellingContent & dc)
creInfo ci;
ci.count = dc.first;
ci.creID = dc.second.size() ? dc.second.back() : CreatureID(-1); //should never be accessed
if (ci.creID != -1)
if (ci.creID != CreatureID::NONE)
{
ci.cre = VLC->creatures()->getById(ci.creID);
ci.level = ci.cre->getLevel(); //this is creature tier, while tryRealize expects dwelling level. Ignore.

View File

@ -57,6 +57,7 @@ using dwellingContent = std::pair<ui32, std::vector<CreatureID>>;
namespace NKAI
{
struct creInfo;
class AIGateway;
class Nullkiller;
const int GOLD_MINE_PRODUCTION = 1000, WOOD_ORE_MINE_PRODUCTION = 2, RESOURCE_MINE_PRODUCTION = 1;
@ -67,7 +68,8 @@ const int ALLOWED_ROAMING_HEROES = 8;
extern const float SAFE_ATTACK_CONSTANT;
extern const int GOLD_RESERVE;
extern boost::thread_specific_ptr<CCallback> cb;
extern thread_local CCallback * cb;
extern thread_local AIGateway * ai;
enum HeroRole
{
@ -149,7 +151,7 @@ struct ObjectIdRef
}
};
template<int id>
template<Obj::Type id>
bool objWithID(const CGObjectInstance * obj)
{
return obj->ID == id;
@ -201,7 +203,7 @@ void foreach_tile_pos(CCallback * cbp, const Func & foo) // avoid costly retriev
template<class Func>
void foreach_neighbour(const int3 & pos, const Func & foo)
{
CCallback * cbp = cb.get(); // avoid costly retrieval of thread-specific pointer
CCallback * cbp = cb; // avoid costly retrieval of thread-specific pointer
for(const int3 & dir : int3::getDirs())
{
const int3 n = pos + dir;
@ -224,7 +226,7 @@ void foreach_neighbour(CCallback * cbp, const int3 & pos, const Func & foo) // a
bool canBeEmbarkmentPoint(const TerrainTile * t, bool fromWater);
bool isObjectPassable(const CGObjectInstance * obj);
bool isObjectPassable(const Nullkiller * ai, const CGObjectInstance * obj);
bool isObjectPassable(const CGObjectInstance * obj, PlayerColor playerColor, PlayerRelations::PlayerRelations objectRelations);
bool isObjectPassable(const CGObjectInstance * obj, PlayerColor playerColor, PlayerRelations objectRelations);
bool isBlockVisitObj(const int3 & pos);
bool isWeeklyRevisitable(const CGObjectInstance * obj);

View File

@ -153,7 +153,7 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
for(auto bonus : *bonusModifiers)
{
// army bonuses will change and object bonuses are temporary
if(bonus->source != BonusSource::ARMY && bonus->source != BonusSource::OBJECT)
if(bonus->source != BonusSource::ARMY && bonus->source != BonusSource::OBJECT_INSTANCE && bonus->source != BonusSource::OBJECT_TYPE)
{
newArmyInstance.addNewBonus(std::make_shared<Bonus>(*bonus));
}
@ -225,7 +225,8 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
if(weakest->count == 1)
{
assert(resultingArmy.size() > 1);
if (resultingArmy.size() == 1)
logAi->warn("Unexpected resulting army size!");
resultingArmy.erase(weakest);
}
@ -255,7 +256,7 @@ std::shared_ptr<CCreatureSet> ArmyManager::getArmyAvailableToBuyAsCCreatureSet(
{
auto ci = infoFromDC(dwelling->creatures[i]);
if(!ci.count || ci.creID == -1)
if(!ci.count || ci.creID == CreatureID::NONE)
continue;
vstd::amin(ci.count, availableRes / ci.cre->getFullRecruitCost()); //max count we can afford
@ -315,7 +316,7 @@ std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
{
auto ci = infoFromDC(dwelling->creatures[i]);
if(ci.creID == -1) continue;
if(ci.creID == CreatureID::NONE) continue;
if(i < GameConstants::CREATURES_PER_TOWN && countGrowth)
{
@ -392,7 +393,7 @@ void ArmyManager::update()
}
}
for(auto army : totalArmy)
for(auto & army : totalArmy)
{
army.second.creature = army.first.toCreature();
army.second.power = evaluateStackPower(army.second.creature, army.second.count);

View File

@ -24,7 +24,7 @@ void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo)
for(auto &pair : townInfo->buildings)
{
if(pair.second->upgrade != -1)
if(pair.second->upgrade != BuildingID::NONE)
{
parentMap[pair.second->upgrade] = pair.first;
}
@ -160,7 +160,7 @@ void BuildAnalyzer::update()
updateDailyIncome();
if(ai->cb->getDate(Date::EDateType::DAY) == 1)
if(ai->cb->getDate(Date::DAY) == 1)
{
goldPreasure = 1;
}
@ -256,7 +256,7 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
{
logAi->trace("cant build. Need other dwelling");
}
else
else if(missingBuildings[0] != toBuild)
{
logAi->trace("cant build. Need %d", missingBuildings[0].num);
@ -274,6 +274,12 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
return prerequisite;
}
else
{
logAi->trace("Cant build. The building requires itself as prerequisite");
return info;
}
}
}
else

View File

@ -72,7 +72,13 @@ void DangerHitMapAnalyzer::updateHitMap()
if(ai->cb->getPlayerRelations(ai->playerID, pair.first) != PlayerRelations::ENEMIES)
continue;
ai->pathfinder->updatePaths(pair.second, PathfinderSettings());
PathfinderSettings ps;
ps.mainTurnDistanceLimit = 10;
ps.scoutTurnDistanceLimit = 10;
ps.useHeroChain = false;
ai->pathfinder->updatePaths(pair.second, ps);
boost::this_thread::interruption_point();

View File

@ -71,7 +71,7 @@ float HeroManager::evaluateSecSkill(SecondarySkill skill, const CGHeroInstance *
float HeroManager::evaluateSpeciality(const CGHeroInstance * hero) const
{
auto heroSpecial = Selector::source(BonusSource::HERO_SPECIAL, hero->type->getIndex());
auto heroSpecial = Selector::source(BonusSource::HERO_SPECIAL, BonusSourceID(hero->type->getId()));
auto secondarySkillBonus = Selector::targetSourceType()(BonusSource::SECONDARY_SKILL);
auto specialSecondarySkillBonuses = hero->getBonuses(heroSpecial.And(secondarySkillBonus));
auto secondarySkillBonuses = hero->getBonuses(Selector::sourceTypeSel(BonusSource::SECONDARY_SKILL));
@ -83,7 +83,7 @@ float HeroManager::evaluateSpeciality(const CGHeroInstance * hero) const
if(hasBonus)
{
SecondarySkill bonusSkill = SecondarySkill(bonus->sid);
SecondarySkill bonusSkill = bonus->sid.as<SecondarySkill>();
float bonusScore = wariorSkillsScores.evaluateSecSkill(hero, bonusSkill);
if(bonusScore > 0)
@ -190,6 +190,41 @@ bool HeroManager::heroCapReached() const
|| heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP);
}
float HeroManager::getMagicStrength(const CGHeroInstance * hero) const
{
auto hasFly = hero->spellbookContainsSpell(SpellID::FLY);
auto hasTownPortal = hero->spellbookContainsSpell(SpellID::TOWN_PORTAL);
auto manaLimit = hero->manaLimit();
auto spellPower = hero->getPrimSkillLevel(PrimarySkill::SPELL_POWER);
auto hasEarth = hero->getSpellSchoolLevel(SpellID(SpellID::TOWN_PORTAL).toSpell()) > 0;
auto score = 0.0f;
for(auto spellId : hero->getSpellsInSpellbook())
{
auto spell = spellId.toSpell();
auto schoolLevel = hero->getSpellSchoolLevel(spell);
score += (spell->getLevel() + 1) * (schoolLevel + 1) * 0.05f;
}
vstd::amin(score, 1);
score *= std::min(1.0f, spellPower / 10.0f);
if(hasFly)
score += 0.3f;
if(hasTownPortal && hasEarth)
score += 0.6f;
vstd::amin(score, 1);
score *= std::min(1.0f, manaLimit / 100.0f);
return std::min(score, 1.0f);
}
bool HeroManager::canRecruitHero(const CGTownInstance * town) const
{
if(!town)
@ -278,7 +313,7 @@ void ExistingSkillRule::evaluateScore(const CGHeroInstance * hero, SecondarySkil
if(heroSkill.first == skill)
return;
upgradesLeft += SecSkillLevel::EXPERT - heroSkill.second;
upgradesLeft += MasteryLevel::EXPERT - heroSkill.second;
}
if(score >= 2 || (score >= 1 && upgradesLeft <= 1))
@ -292,7 +327,7 @@ void WisdomRule::evaluateScore(const CGHeroInstance * hero, SecondarySkill skill
auto wisdomLevel = hero->getSecSkillLevel(SecondarySkill::WISDOM);
if(hero->level > 10 && wisdomLevel == SecSkillLevel::NONE)
if(hero->level > 10 && wisdomLevel == MasteryLevel::NONE)
score += 1.5;
}
@ -310,7 +345,7 @@ void AtLeastOneMagicRule::evaluateScore(const CGHeroInstance * hero, SecondarySk
bool heroHasAnyMagic = vstd::contains_if(magicSchools, [&](SecondarySkill skill) -> bool
{
return hero->getSecSkillLevel(skill) > SecSkillLevel::NONE;
return hero->getSecSkillLevel(skill) > MasteryLevel::NONE;
});
if(!heroHasAnyMagic)

View File

@ -34,6 +34,7 @@ public:
virtual bool heroCapReached() const = 0;
virtual const CGHeroInstance * findHeroWithGrail() const = 0;
virtual const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const = 0;
virtual float getMagicStrength(const CGHeroInstance * hero) const = 0;
};
class DLL_EXPORT ISecondarySkillRule
@ -76,6 +77,7 @@ public:
bool heroCapReached() const override;
const CGHeroInstance * findHeroWithGrail() const override;
const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const override;
float getMagicStrength(const CGHeroInstance * hero) const override;
private:
float evaluateFightingStrength(const CGHeroInstance * hero) const;

View File

@ -94,16 +94,22 @@ const CGObjectInstance * ObjectClusterizer::getBlocker(const AIPath & path) cons
{
for(auto node = path.nodes.rbegin(); node != path.nodes.rend(); node++)
{
auto guardPos = ai->cb->getGuardingCreaturePosition(node->coord);
auto blockers = ai->cb->getVisitableObjs(node->coord);
if(guardPos.valid())
{
auto guard = ai->cb->getTopObj(ai->cb->getGuardingCreaturePosition(node->coord));
std::vector<const CGObjectInstance *> blockers = {};
if(guard)
if(node->layer == EPathfindingLayer::LAND || node->layer == EPathfindingLayer::SAIL)
{
auto guardPos = ai->cb->getGuardingCreaturePosition(node->coord);
blockers = ai->cb->getVisitableObjs(node->coord);
if(guardPos.valid())
{
blockers.insert(blockers.begin(), guard);
auto guard = ai->cb->getTopObj(ai->cb->getGuardingCreaturePosition(node->coord));
if(guard)
{
blockers.insert(blockers.begin(), guard);
}
}
}

View File

@ -20,9 +20,6 @@
namespace NKAI
{
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
using namespace Goals;
std::string BuildingBehavior::toString() const

View File

@ -17,9 +17,6 @@
namespace NKAI
{
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
using namespace Goals;
std::string BuyArmyBehavior::toString() const

View File

@ -19,9 +19,6 @@
namespace NKAI
{
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
using namespace Goals;
template <typename T>

View File

@ -19,9 +19,6 @@
namespace NKAI
{
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
using namespace Goals;
std::string ClusterBehavior::toString() const

View File

@ -25,9 +25,6 @@
namespace NKAI
{
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
const float TREAT_IGNORE_RATIO = 2;
using namespace Goals;
@ -114,7 +111,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa
if(ai->nullkiller->isHeroLocked(town->garrisonHero.get()))
{
logAi->trace(
"Hero %s in garrison of town %s is suposed to defend the town",
"Hero %s in garrison of town %s is supposed to defend the town",
town->garrisonHero->getNameTranslated(),
town->getNameTranslated());

View File

@ -23,9 +23,6 @@
namespace NKAI
{
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
using namespace Goals;
std::string GatherArmyBehavior::toString() const

View File

@ -17,9 +17,6 @@
namespace NKAI
{
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
using namespace Goals;
std::string RecruitHeroBehavior::toString() const
@ -84,7 +81,7 @@ Goals::TGoalVec RecruitHeroBehavior::decompose() const
}
}
if(treasureSourcesCount < 5)
if(treasureSourcesCount < 5 && (town->garrisonHero || town->getUpperArmy()->getArmyStrength() < 10000))
continue;
if(cb->getHeroesInfo().size() < cb->getTownsInfo().size() + 1

View File

@ -21,9 +21,6 @@
namespace NKAI
{
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
using namespace Goals;
std::string StartupBehavior::toString() const

View File

@ -0,0 +1,70 @@
/*
* StartupBehavior.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "StayAtTownBehavior.h"
#include "../AIGateway.h"
#include "../AIUtility.h"
#include "../Goals/StayAtTown.h"
#include "../Goals/Composition.h"
#include "../Goals/ExecuteHeroChain.h"
#include "lib/mapObjects/MapObjects.h" //for victory conditions
#include "../Engine/Nullkiller.h"
namespace NKAI
{
using namespace Goals;
std::string StayAtTownBehavior::toString() const
{
return "StayAtTownBehavior";
}
Goals::TGoalVec StayAtTownBehavior::decompose() const
{
Goals::TGoalVec tasks;
auto towns = cb->getTownsInfo();
if(!towns.size())
return tasks;
for(auto town : towns)
{
if(!town->hasBuilt(BuildingID::MAGES_GUILD_1))
continue;
auto paths = ai->nullkiller->pathfinder->getPathInfo(town->visitablePos());
for(auto & path : paths)
{
if(town->visitingHero && town->visitingHero.get() != path.targetHero)
continue;
if(path.turn() == 0 && !path.getFirstBlockedAction() && path.exchangeCount <= 1)
{
if(path.targetHero->mana == path.targetHero->manaLimit())
continue;
Composition stayAtTown;
stayAtTown.addNextSequence({
sptr(ExecuteHeroChain(path)),
sptr(StayAtTown(town, path))
});
tasks.push_back(sptr(stayAtTown));
}
}
}
return tasks;
}
}

View File

@ -0,0 +1,39 @@
/*
* StayAtTownBehavior.h, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#pragma once
#include "lib/VCMI_Lib.h"
#include "../Goals/CGoal.h"
#include "../AIUtility.h"
namespace NKAI
{
namespace Goals
{
class StayAtTownBehavior : public CGoal<StayAtTownBehavior>
{
public:
StayAtTownBehavior()
:CGoal(STAY_AT_TOWN_BEHAVIOR)
{
}
virtual TGoalVec decompose() const override;
virtual std::string toString() const override;
virtual bool operator==(const StayAtTownBehavior & other) const override
{
return true;
}
};
}
}

View File

@ -9,6 +9,7 @@ set(Nullkiller_SRCS
Pathfinding/Actions/BuyArmyAction.cpp
Pathfinding/Actions/BoatActions.cpp
Pathfinding/Actions/TownPortalAction.cpp
Pathfinding/Actions/AdventureSpellCastMovementActions.cpp
Pathfinding/Rules/AILayerTransitionRule.cpp
Pathfinding/Rules/AIMovementAfterDestinationRule.cpp
Pathfinding/Rules/AIMovementToDestinationRule.cpp
@ -34,6 +35,7 @@ set(Nullkiller_SRCS
Goals/ExecuteHeroChain.cpp
Goals/ExchangeSwapTownHeroes.cpp
Goals/CompleteQuest.cpp
Goals/StayAtTown.cpp
Markers/ArmyUpgrade.cpp
Markers/HeroExchange.cpp
Markers/UnlockCluster.cpp
@ -52,6 +54,7 @@ set(Nullkiller_SRCS
Behaviors/BuildingBehavior.cpp
Behaviors/GatherArmyBehavior.cpp
Behaviors/ClusterBehavior.cpp
Behaviors/StayAtTownBehavior.cpp
Helpers/ArmyFormation.cpp
AIGateway.cpp
)
@ -69,6 +72,7 @@ set(Nullkiller_HEADERS
Pathfinding/Actions/BuyArmyAction.h
Pathfinding/Actions/BoatActions.h
Pathfinding/Actions/TownPortalAction.h
Pathfinding/Actions/AdventureSpellCastMovementActions.h
Pathfinding/Rules/AILayerTransitionRule.h
Pathfinding/Rules/AIMovementAfterDestinationRule.h
Pathfinding/Rules/AIMovementToDestinationRule.h
@ -97,6 +101,7 @@ set(Nullkiller_HEADERS
Goals/ExchangeSwapTownHeroes.h
Goals/CompleteQuest.h
Goals/Goals.h
Goals/StayAtTown.h
Markers/ArmyUpgrade.h
Markers/HeroExchange.h
Markers/UnlockCluster.h
@ -115,6 +120,7 @@ set(Nullkiller_HEADERS
Behaviors/BuildingBehavior.h
Behaviors/GatherArmyBehavior.h
Behaviors/ClusterBehavior.h
Behaviors/StayAtTownBehavior.h
Helpers/ArmyFormation.h
AIGateway.h
)

View File

@ -24,9 +24,6 @@
namespace NKAI
{
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
using namespace Goals;
void DeepDecomposer::reset()

View File

@ -20,8 +20,6 @@ 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
extern boost::thread_specific_ptr<AIGateway> ai;
engineBase::engineBase()
{
rules = new fl::RuleBlock();

View File

@ -8,7 +8,11 @@
*
*/
#pragma once
#include <fl/Headers.h>
#if __has_include(<fuzzylite/Headers.h>)
# include <fuzzylite/Headers.h>
#else
# include <fl/Headers.h>
#endif
#include "../Goals/AbstractGoal.h"
VCMI_LIB_NAMESPACE_BEGIN

View File

@ -111,7 +111,7 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj)
{
auto cb = ai->cb.get();
if(obj->tempOwner < PlayerColor::PLAYER_LIMIT && cb->getPlayerRelations(obj->tempOwner, ai->playerID) != PlayerRelations::ENEMIES) //owned or allied objects don't pose any threat
if(obj->tempOwner.isValidPlayer() && cb->getPlayerRelations(obj->tempOwner, ai->playerID) != PlayerRelations::ENEMIES) //owned or allied objects don't pose any threat
return 0;
switch(obj->ID)

View File

@ -18,15 +18,13 @@
#include "../Behaviors/BuildingBehavior.h"
#include "../Behaviors/GatherArmyBehavior.h"
#include "../Behaviors/ClusterBehavior.h"
#include "../Behaviors/StayAtTownBehavior.h"
#include "../Goals/Invalid.h"
#include "../Goals/Composition.h"
namespace NKAI
{
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
using namespace Goals;
#if NKAI_TRACE_LEVEL >= 1
@ -141,8 +139,8 @@ void Nullkiller::updateAiState(int pass, bool fast)
{
memory->removeInvisibleObjects(cb.get());
dangerHitMap->calculateTileOwners();
dangerHitMap->updateHitMap();
dangerHitMap->calculateTileOwners();
boost::this_thread::interruption_point();
@ -265,7 +263,8 @@ void Nullkiller::makeTurn()
choseBestTask(sptr(CaptureObjectsBehavior()), 1),
choseBestTask(sptr(ClusterBehavior()), MAX_DEPTH),
choseBestTask(sptr(DefenceBehavior()), MAX_DEPTH),
choseBestTask(sptr(GatherArmyBehavior()), MAX_DEPTH)
choseBestTask(sptr(GatherArmyBehavior()), MAX_DEPTH),
choseBestTask(sptr(StayAtTownBehavior()), MAX_DEPTH)
};
if(cb->getDate(Date::DAY) == 1)
@ -341,7 +340,7 @@ void Nullkiller::executeTask(Goals::TTask task)
try
{
task->accept(ai.get());
task->accept(ai);
logAi->trace("Task %s completed in %lld", taskDescr, timeElapsed(start));
}
catch(goalFulfilledException &)

View File

@ -22,6 +22,7 @@
#include "../../../lib/filesystem/Filesystem.h"
#include "../Goals/ExecuteHeroChain.h"
#include "../Goals/BuildThis.h"
#include "../Goals/StayAtTown.h"
#include "../Goals/ExchangeSwapTownHeroes.h"
#include "../Goals/DismissHero.h"
#include "../Markers/UnlockCluster.h"
@ -68,7 +69,7 @@ PriorityEvaluator::~PriorityEvaluator()
void PriorityEvaluator::initVisitTile()
{
auto file = CResourceHandler::get()->load(ResourceID("config/ai/object-priorities.txt"))->readAll();
auto file = CResourceHandler::get()->load(ResourcePath("config/ai/object-priorities.txt"))->readAll();
std::string str = std::string((char *)file.first.get(), file.second);
engine = fl::FllImporter().fromString(str);
armyLossPersentageVariable = engine->getInputVariable("armyLoss");
@ -241,13 +242,13 @@ uint64_t evaluateArtifactArmyValue(CArtifactInstance * art)
return 1500;
auto statsValue =
10 * art->valOfBonuses(BonusType::MOVEMENT, 1)
10 * art->valOfBonuses(BonusType::MOVEMENT, BonusCustomSubtype::heroMovementLand)
+ 1200 * art->valOfBonuses(BonusType::STACKS_SPEED)
+ 700 * art->valOfBonuses(BonusType::MORALE)
+ 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, PrimarySkill::ATTACK)
+ 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, PrimarySkill::DEFENSE)
+ 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, PrimarySkill::KNOWLEDGE)
+ 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, PrimarySkill::SPELL_POWER)
+ 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::ATTACK))
+ 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::DEFENSE))
+ 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::KNOWLEDGE))
+ 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::SPELL_POWER))
+ 500 * art->valOfBonuses(BonusType::LUCK);
auto classValue = 0;
@ -309,6 +310,9 @@ uint64_t RewardEvaluator::getArmyReward(
: 0;
case Obj::PANDORAS_BOX:
return 5000;
case Obj::MAGIC_WELL:
case Obj::MAGIC_SPRING:
return getManaRecoveryArmyReward(hero);
default:
return 0;
}
@ -450,6 +454,11 @@ uint64_t RewardEvaluator::townArmyGrowth(const CGTownInstance * town) const
return result;
}
uint64_t RewardEvaluator::getManaRecoveryArmyReward(const CGHeroInstance * hero) const
{
return ai->heroManager->getMagicStrength(hero) * 10000 * (1.0f - std::sqrt(static_cast<float>(hero->mana) / hero->manaLimit()));
}
float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) const
{
if(!target)
@ -519,14 +528,17 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons
}
}
float RewardEvaluator::evaluateWitchHutSkillScore(const CGWitchHut * hut, const CGHeroInstance * hero, HeroRole role) const
float RewardEvaluator::evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const
{
auto rewardable = dynamic_cast<const CRewardableObject *>(hut);
assert(rewardable);
auto skill = SecondarySkill(*rewardable->configuration.getVariable("secondarySkill", "gainedSkill"));
if(!hut->wasVisited(hero->tempOwner))
return role == HeroRole::SCOUT ? 2 : 0;
auto skill = SecondarySkill(hut->ability);
if(hero->getSecSkillLevel(skill) != SecSkillLevel::NONE
if(hero->getSecSkillLevel(skill) != MasteryLevel::NONE
|| hero->secSkills.size() >= GameConstants::SKILL_PER_HERO)
return 0;
@ -566,7 +578,7 @@ float RewardEvaluator::getSkillReward(const CGObjectInstance * target, const CGH
case Obj::LIBRARY_OF_ENLIGHTENMENT:
return 8;
case Obj::WITCH_HUT:
return evaluateWitchHutSkillScore(dynamic_cast<const CGWitchHut *>(target), hero, role);
return evaluateWitchHutSkillScore(target, hero, role);
case Obj::PANDORAS_BOX:
//Can contains experience, spells, or skills (only on custom maps)
return 2.5f;
@ -693,6 +705,22 @@ public:
}
};
class StayAtTownManaRecoveryEvaluator : public IEvaluationContextBuilder
{
public:
virtual void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
{
if(task->goalType != Goals::STAY_AT_TOWN)
return;
Goals::StayAtTown & stayAtTown = dynamic_cast<Goals::StayAtTown &>(*task);
evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero().get());
evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted();
evaluationContext.movementCost += stayAtTown.getMovementWasted();
}
};
void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uint8_t turn, uint64_t ourStrength)
{
HitMapInfo enemyDanger = evaluationContext.evaluator.getEnemyHeroDanger(tile, turn);
@ -998,6 +1026,7 @@ PriorityEvaluator::PriorityEvaluator(const Nullkiller * ai)
evaluationContextBuilders.push_back(std::make_shared<DefendTownEvaluator>());
evaluationContextBuilders.push_back(std::make_shared<ExchangeSwapTownHeroesContextBuilder>());
evaluationContextBuilders.push_back(std::make_shared<DismissHeroContextBuilder>(ai));
evaluationContextBuilders.push_back(std::make_shared<StayAtTownManaRecoveryEvaluator>());
}
EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal) const

View File

@ -8,14 +8,16 @@
*
*/
#pragma once
#include "fl/Headers.h"
#if __has_include(<fuzzylite/Headers.h>)
# include <fuzzylite/Headers.h>
#else
# include <fl/Headers.h>
#endif
#include "../Goals/CGoal.h"
#include "../Pathfinding/AIPathfinder.h"
VCMI_LIB_NAMESPACE_BEGIN
class CGWitchHut;
VCMI_LIB_NAMESPACE_END
namespace NKAI
@ -39,12 +41,13 @@ public:
float getResourceRequirementStrength(int resType) const;
float getStrategicalValue(const CGObjectInstance * target) const;
float getTotalResourceRequirementStrength(int resType) const;
float evaluateWitchHutSkillScore(const CGWitchHut * hut, const CGHeroInstance * hero, HeroRole role) const;
float evaluateWitchHutSkillScore(const CGObjectInstance * hut, const CGHeroInstance * hero, HeroRole role) const;
float getSkillReward(const CGObjectInstance * target, const CGHeroInstance * hero, HeroRole role) const;
int32_t getGoldReward(const CGObjectInstance * target, const CGHeroInstance * hero) const;
uint64_t getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const;
const HitMapInfo & getEnemyHeroDanger(const int3 & tile, uint8_t turn) const;
uint64_t townArmyGrowth(const CGTownInstance * town) const;
uint64_t getManaRecoveryArmyReward(const CGHeroInstance * hero) const;
};
struct DLL_EXPORT EvaluationContext

View File

@ -10,14 +10,11 @@
#include "StdInc.h"
#include "AbstractGoal.h"
#include "../AIGateway.h"
#include "../../../lib/StringConstants.h"
#include "../../../lib/constants/StringConstants.h"
namespace NKAI
{
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
using namespace Goals;
TSubgoal Goals::sptr(const AbstractGoal & tmp)

View File

@ -71,7 +71,9 @@ namespace Goals
ARMY_UPGRADE,
DEFEND_TOWN,
CAPTURE_OBJECT,
SAVE_RESOURCES
SAVE_RESOURCES,
STAY_AT_TOWN_BEHAVIOR,
STAY_AT_TOWN
};
class DLL_EXPORT TSubgoal : public std::shared_ptr<AbstractGoal>

View File

@ -14,9 +14,6 @@
namespace NKAI
{
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
using namespace Goals;
bool AdventureSpellCast::operator==(const AdventureSpellCast & other) const

View File

@ -15,9 +15,6 @@
namespace NKAI
{
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
using namespace Goals;
bool BuildBoat::operator==(const BuildBoat & other) const

View File

@ -11,18 +11,14 @@
#include "BuildThis.h"
#include "../AIGateway.h"
#include "../AIUtility.h"
#include "../../../lib/StringConstants.h"
#include "../../../lib/constants/StringConstants.h"
namespace NKAI
{
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
using namespace Goals;
BuildThis::BuildThis(BuildingID Bid, const CGTownInstance * tid)
: ElementarGoal(Goals::BUILD_STRUCTURE)
{

View File

@ -17,9 +17,6 @@
namespace NKAI
{
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
using namespace Goals;
bool BuyArmy::operator==(const BuyArmy & other) const
@ -54,7 +51,7 @@ void BuyArmy::accept(AIGateway * ai)
auto res = cb->getResourceAmount();
auto & ci = armyToBuy[i];
if(objid != -1 && ci.creID != objid)
if(objid != CreatureID::NONE && ci.creID.getNum() != objid)
continue;
vstd::amin(ci.count, res / ci.cre->getFullRecruitCost());

View File

@ -18,8 +18,6 @@
namespace NKAI
{
extern boost::thread_specific_ptr<CCallback> cb;
using namespace Goals;
bool CaptureObject::operator==(const CaptureObject & other) const

View File

@ -17,9 +17,6 @@
namespace NKAI
{
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
using namespace Goals;
bool isKeyMaster(const QuestInfo & q)
@ -40,42 +37,29 @@ TGoalVec CompleteQuest::decompose() const
}
logAi->debug("Trying to realize quest: %s", questToString());
switch(q.quest->missionType)
{
case CQuest::MISSION_ART:
if(!q.quest->mission.artifacts.empty())
return missionArt();
case CQuest::MISSION_HERO:
if(!q.quest->mission.heroes.empty())
return missionHero();
case CQuest::MISSION_ARMY:
if(!q.quest->mission.creatures.empty())
return missionArmy();
case CQuest::MISSION_RESOURCES:
if(q.quest->mission.resources.nonZero())
return missionResources();
case CQuest::MISSION_KILL_HERO:
case CQuest::MISSION_KILL_CREATURE:
if(q.quest->killTarget != ObjectInstanceID::NONE)
return missionDestroyObj();
case CQuest::MISSION_PRIMARY_STAT:
return missionIncreasePrimaryStat();
for(auto & s : q.quest->mission.primary)
if(s)
return missionIncreasePrimaryStat();
case CQuest::MISSION_LEVEL:
if(q.quest->mission.heroLevel > 0)
return missionLevel();
case CQuest::MISSION_PLAYER:
if(ai->playerID.getNum() != q.quest->m13489val)
logAi->debug("Can't be player of color %d", q.quest->m13489val);
break;
case CQuest::MISSION_KEYMASTER:
return missionKeymaster();
} //end of switch
return TGoalVec();
}
@ -110,7 +94,7 @@ std::string CompleteQuest::questToString() const
return "find " + VLC->generaltexth->tentColors[q.obj->subID] + " keymaster tent";
}
if(q.quest->missionType == CQuest::MISSION_NONE)
if(q.quest->questName == CQuest::missionName(0))
return "inactive quest";
MetaString ms;
@ -140,7 +124,7 @@ TGoalVec CompleteQuest::missionArt() const
CaptureObjectsBehavior findArts;
for(auto art : q.quest->m5arts)
for(auto art : q.quest->mission.artifacts)
{
solutions.push_back(sptr(CaptureObjectsBehavior().ofType(Obj::ARTIFACT, art)));
}
@ -226,7 +210,7 @@ TGoalVec CompleteQuest::missionResources() const
TGoalVec CompleteQuest::missionDestroyObj() const
{
auto obj = cb->getObjByQuestIdentifier(q.quest->m13489val);
auto obj = cb->getObjByQuestIdentifier(q.quest->killTarget);
if(!obj)
return CaptureObjectsBehavior(q.obj).decompose();

View File

@ -11,15 +11,12 @@
#include "Composition.h"
#include "../AIGateway.h"
#include "../AIUtility.h"
#include "../../../lib/StringConstants.h"
#include "../../../lib/constants/StringConstants.h"
namespace NKAI
{
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
using namespace Goals;
bool Composition::operator==(const Composition & other) const

View File

@ -16,9 +16,6 @@
namespace NKAI
{
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
using namespace Goals;
bool DigAtTile::operator==(const DigAtTile & other) const

View File

@ -14,9 +14,6 @@
namespace NKAI
{
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
using namespace Goals;
bool DismissHero::operator==(const DismissHero & other) const

View File

@ -16,9 +16,6 @@
namespace NKAI
{
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
using namespace Goals;
ExchangeSwapTownHeroes::ExchangeSwapTownHeroes(

View File

@ -15,9 +15,6 @@
namespace NKAI
{
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
using namespace Goals;
ExecuteHeroChain::ExecuteHeroChain(const AIPath & path, const CGObjectInstance * obj)

View File

@ -11,15 +11,12 @@
#include "Goals.h"
#include "../AIGateway.h"
#include "../AIUtility.h"
#include "../../../lib/StringConstants.h"
#include "../../../lib/constants/StringConstants.h"
namespace NKAI
{
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
using namespace Goals;
std::string RecruitHero::toString() const

View File

@ -15,9 +15,6 @@
namespace NKAI
{
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
using namespace Goals;
bool SaveResources::operator==(const SaveResources & other) const

View File

@ -0,0 +1,52 @@
/*
* ArmyUpgrade.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "StayAtTown.h"
#include "../AIGateway.h"
#include "../Engine/Nullkiller.h"
#include "../AIUtility.h"
namespace NKAI
{
using namespace Goals;
StayAtTown::StayAtTown(const CGTownInstance * town, AIPath & path)
: ElementarGoal(Goals::STAY_AT_TOWN)
{
sethero(path.targetHero);
settown(town);
movementWasted = static_cast<float>(hero->movementPointsRemaining()) / hero->movementPointsLimit(!hero->boat) - path.movementCost();
vstd::amax(movementWasted, 0);
}
bool StayAtTown::operator==(const StayAtTown & other) const
{
return hero == other.hero && town == other.town;
}
std::string StayAtTown::toString() const
{
return "Stay at town " + town->getNameTranslated()
+ " hero " + hero->getNameTranslated()
+ ", mana: " + std::to_string(hero->mana);
}
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.get(), HeroLockedReason::DEFENCE);
}
}

View File

@ -0,0 +1,36 @@
/*
* ArmyUpgrade.h, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#pragma once
#include "../Goals/CGoal.h"
#include "../Pathfinding/AINodeStorage.h"
#include "../Analyzers/ArmyManager.h"
#include "../Analyzers/DangerHitMapAnalyzer.h"
namespace NKAI
{
namespace Goals
{
class DLL_EXPORT StayAtTown : public ElementarGoal<StayAtTown>
{
private:
float movementWasted;
public:
StayAtTown(const CGTownInstance * town, AIPath & path);
virtual bool operator==(const StayAtTown & other) const override;
virtual std::string toString() const override;
void accept(AIGateway * ai) override;
float getMovementWasted() const { return movementWasted; }
};
}
}

View File

@ -16,9 +16,6 @@
namespace NKAI
{
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
using namespace Goals;
ArmyUpgrade::ArmyUpgrade(const AIPath & upgradePath, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade)

View File

@ -17,9 +17,6 @@
namespace NKAI
{
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
using namespace Goals;
bool HeroExchange::operator==(const HeroExchange & other) const

View File

@ -16,9 +16,6 @@
namespace NKAI
{
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
using namespace Goals;
bool UnlockCluster::operator==(const UnlockCluster & other) const

View File

@ -279,9 +279,10 @@ void AINodeStorage::commit(
#if NKAI_PATHFINDER_TRACE_LEVEL >= 2
logAi->trace(
"Commited %s -> %s, cost: %f, turn: %s, mp: %d, hero: %s, mask: %x, army: %lld",
"Commited %s -> %s, layer: %d, cost: %f, turn: %s, mp: %d, hero: %s, mask: %x, army: %lld",
source->coord.toString(),
destination->coord.toString(),
destination->layer,
destination->getCost(),
std::to_string(destination->turns),
destination->moveRemains,
@ -983,7 +984,7 @@ std::vector<CGPathNode *> AINodeStorage::calculateTeleportations(
struct TowmPortalFinder
{
const std::vector<CGPathNode *> & initialNodes;
SecSkillLevel::SecSkillLevel townPortalSkillLevel;
MasteryLevel::Type townPortalSkillLevel;
uint64_t movementNeeded;
const ChainActor * actor;
const CGHeroInstance * hero;
@ -1005,8 +1006,8 @@ struct TowmPortalFinder
townPortal = spellID.toSpell();
// TODO: Copy/Paste from TownPortalMechanics
townPortalSkillLevel = SecSkillLevel::SecSkillLevel(hero->getSpellSchoolLevel(townPortal));
movementNeeded = GameConstants::BASE_MOVEMENT_COST * (townPortalSkillLevel >= SecSkillLevel::EXPERT ? 2 : 3);
townPortalSkillLevel = MasteryLevel::Type(hero->getSpellSchoolLevel(townPortal));
movementNeeded = GameConstants::BASE_MOVEMENT_COST * (townPortalSkillLevel >= MasteryLevel::EXPERT ? 2 : 3);
}
bool actorCanCastTownPortal()
@ -1027,7 +1028,7 @@ struct TowmPortalFinder
continue;
}
if(townPortalSkillLevel < SecSkillLevel::ADVANCED)
if(townPortalSkillLevel < MasteryLevel::ADVANCED)
{
const CGTownInstance * nearestTown = *vstd::minElementByFun(targetTowns, [&](const CGTownInstance * t) -> int
{
@ -1208,7 +1209,7 @@ bool AINodeStorage::hasBetterChain(
"Block ineficient battle move %s->%s, hero: %s[%X], army %lld, mp diff: %i",
source->coord.toString(),
candidateNode->coord.toString(),
candidateNode->actor->hero->name,
candidateNode->actor->hero->getNameTranslated(),
candidateNode->actor->chainMask,
candidateNode->actor->armyValue,
node.moveRemains - candidateNode->moveRemains);
@ -1232,7 +1233,7 @@ bool AINodeStorage::hasBetterChain(
"Block ineficient move because of stronger army %s->%s, hero: %s[%X], army %lld, mp diff: %i",
source->coord.toString(),
candidateNode->coord.toString(),
candidateNode->actor->hero->name,
candidateNode->actor->hero->getNameTranslated(),
candidateNode->actor->chainMask,
candidateNode->actor->armyValue,
node.moveRemains - candidateNode->moveRemains);
@ -1258,7 +1259,7 @@ bool AINodeStorage::hasBetterChain(
"Block ineficient move because of stronger hero %s->%s, hero: %s[%X], army %lld, mp diff: %i",
source->coord.toString(),
candidateNode->coord.toString(),
candidateNode->actor->hero->name,
candidateNode->actor->hero->getNameTranslated(),
candidateNode->actor->chainMask,
candidateNode->actor->armyValue,
node.moveRemains - candidateNode->moveRemains);
@ -1343,6 +1344,7 @@ void AINodeStorage::fillChainInfo(const AIPathNode * node, AIPath & path, int pa
pathNode.coord = node->coord;
pathNode.parentIndex = parentIndex;
pathNode.actionIsBlocked = false;
pathNode.layer = node->layer;
if(pathNode.specialAction)
{

View File

@ -45,7 +45,7 @@ struct AIPathNode : public CGPathNode
{
uint64_t danger;
uint64_t armyLoss;
uint32_t manaCost;
int32_t manaCost;
const AIPathNode * chainOther;
std::shared_ptr<const SpecialAction> specialAction;
const ChainActor * actor;
@ -65,6 +65,7 @@ struct AIPathNodeInfo
float cost;
uint8_t turns;
int3 coord;
EPathfindingLayer layer;
uint64_t danger;
const CGHeroInstance * targetHero;
int parentIndex;

View File

@ -44,6 +44,7 @@ namespace AIPathfinding
std::shared_ptr<AINodeStorage> nodeStorage)
:PathfinderConfig(nodeStorage, makeRuleset(cb, ai, nodeStorage)), aiNodeStorage(nodeStorage)
{
options.canUseCast = true;
}
AIPathfinderConfig::~AIPathfinderConfig() = default;

View File

@ -0,0 +1,82 @@
/*
* AdventureSpellCastMovementActions.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "../../AIGateway.h"
#include "../../Goals/AdventureSpellCast.h"
#include "../../Goals/CaptureObject.h"
#include "../../Goals/Invalid.h"
#include "../../Goals/BuildBoat.h"
#include "../../../../lib/mapObjects/MapObjects.h"
#include "AdventureSpellCastMovementActions.h"
namespace NKAI
{
namespace AIPathfinding
{
AdventureCastAction::AdventureCastAction(SpellID spellToCast, const CGHeroInstance * hero)
:spellToCast(spellToCast), hero(hero)
{
manaCost = hero->getSpellCost(spellToCast.toSpell());
}
WaterWalkingAction::WaterWalkingAction(const CGHeroInstance * hero)
:AdventureCastAction(SpellID::WATER_WALK, hero)
{ }
AirWalkingAction::AirWalkingAction(const CGHeroInstance * hero)
: AdventureCastAction(SpellID::FLY, hero)
{
}
void AdventureCastAction::applyOnDestination(
const CGHeroInstance * hero,
CDestinationNodeInfo & destination,
const PathNodeInfo & source,
AIPathNode * dstMode,
const AIPathNode * srcNode) const
{
dstMode->manaCost = srcNode->manaCost + manaCost;
dstMode->theNodeBefore = source.node;
}
void AdventureCastAction::execute(const CGHeroInstance * hero) const
{
assert(hero == this->hero);
Goals::AdventureSpellCast(hero, spellToCast).accept(ai);
}
bool AdventureCastAction::canAct(const AIPathNode * source) const
{
assert(hero == this->hero);
auto hero = source->actor->hero;
#ifdef VCMI_TRACE_PATHFINDER
logAi->trace(
"Hero %s has %d mana and needed %d and already spent %d",
hero->name,
hero->mana,
getManaCost(hero),
source->manaCost);
#endif
return hero->mana >= source->manaCost + manaCost;
}
std::string AdventureCastAction::toString() const
{
return "Cast " + spellToCast.toSpell()->getNameTranslated() + " by " + hero->getNameTranslated();
}
}
}

View File

@ -0,0 +1,58 @@
/*
* AdventureSpellCastMovementActions.h, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#pragma once
#include "SpecialAction.h"
#include "../../../../lib/mapObjects/MapObjects.h"
namespace NKAI
{
namespace AIPathfinding
{
class AdventureCastAction : public SpecialAction
{
private:
SpellID spellToCast;
const CGHeroInstance * hero;
int manaCost;
public:
AdventureCastAction(SpellID spellToCast, const CGHeroInstance * hero);
virtual void execute(const CGHeroInstance * hero) const override;
virtual void applyOnDestination(
const CGHeroInstance * hero,
CDestinationNodeInfo & destination,
const PathNodeInfo & source,
AIPathNode * dstMode,
const AIPathNode * srcNode) const override;
virtual bool canAct(const AIPathNode * source) const override;
virtual std::string toString() const override;
};
class WaterWalkingAction : public AdventureCastAction
{
public:
WaterWalkingAction(const CGHeroInstance * hero);
};
class AirWalkingAction : public AdventureCastAction
{
public:
AirWalkingAction(const CGHeroInstance * hero);
};
}
}

View File

@ -16,9 +16,6 @@
namespace NKAI
{
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
namespace AIPathfinding
{
void BattleAction::execute(const CGHeroInstance * hero) const

View File

@ -20,14 +20,11 @@
namespace NKAI
{
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
namespace AIPathfinding
{
void BuildBoatAction::execute(const CGHeroInstance * hero) const
{
return Goals::BuildBoat(shipyard).accept(ai.get());
return Goals::BuildBoat(shipyard).accept(ai);
}
Goals::TSubgoal BuildBoatAction::decompose(const CGHeroInstance * hero) const
@ -80,7 +77,7 @@ namespace AIPathfinding
void SummonBoatAction::execute(const CGHeroInstance * hero) const
{
Goals::AdventureSpellCast(hero, SpellID::SUMMON_BOAT).accept(ai.get());
Goals::AdventureSpellCast(hero, SpellID::SUMMON_BOAT).accept(ai);
}
const ChainActor * SummonBoatAction::getActor(const ChainActor * sourceActor) const
@ -117,7 +114,7 @@ namespace AIPathfinding
source->manaCost);
#endif
return hero->mana >= (si32)(source->manaCost + getManaCost(hero));
return hero->mana >= source->manaCost + getManaCost(hero);
}
std::string SummonBoatAction::toString() const
@ -125,7 +122,7 @@ namespace AIPathfinding
return "Summon Boat";
}
uint32_t SummonBoatAction::getManaCost(const CGHeroInstance * hero) const
int32_t SummonBoatAction::getManaCost(const CGHeroInstance * hero) const
{
SpellID summonBoat = SpellID::SUMMON_BOAT;

View File

@ -20,8 +20,6 @@ namespace AIPathfinding
{
class VirtualBoatAction : public SpecialAction
{
public:
virtual const ChainActor * getActor(const ChainActor * sourceActor) const = 0;
};
class SummonBoatAction : public VirtualBoatAction
@ -43,7 +41,7 @@ namespace AIPathfinding
virtual std::string toString() const override;
private:
uint32_t getManaCost(const CGHeroInstance * hero) const;
int32_t getManaCost(const CGHeroInstance * hero) const;
};
class BuildBoatAction : public VirtualBoatAction

View File

@ -16,9 +16,6 @@
namespace NKAI
{
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
namespace AIPathfinding
{
void BuyArmyAction::execute(const CGHeroInstance * hero) const

View File

@ -16,9 +16,6 @@
namespace NKAI
{
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
namespace AIPathfinding
{
bool QuestAction::canAct(const AIPathNode * node) const
@ -28,7 +25,7 @@ namespace AIPathfinding
return dynamic_cast<const IQuestObject *>(questInfo.obj)->checkQuest(node->actor->hero);
}
return questInfo.quest->progress == CQuest::NOT_ACTIVE
return questInfo.quest->activeForPlayers.count(node->actor->hero->getOwner())
|| questInfo.quest->checkQuest(node->actor->hero);
}

View File

@ -22,6 +22,7 @@ namespace NKAI
{
struct AIPathNode;
class ChainActor;
class SpecialAction
{
@ -54,6 +55,11 @@ public:
{
return {};
}
virtual const ChainActor * getActor(const ChainActor * sourceActor) const
{
return sourceActor;
}
};
class CompositeAction : public SpecialAction

View File

@ -18,9 +18,6 @@ namespace NKAI
using namespace AIPathfinding;
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
void TownPortalAction::execute(const CGHeroInstance * hero) const
{
auto goal = Goals::AdventureSpellCast(hero, SpellID::TOWN_PORTAL);
@ -28,7 +25,7 @@ void TownPortalAction::execute(const CGHeroInstance * hero) const
goal.town = target;
goal.tile = target->visitablePos();
goal.accept(ai.get());
goal.accept(ai);
}
std::string TownPortalAction::toString() const

View File

@ -10,6 +10,8 @@
#include "StdInc.h"
#include "AILayerTransitionRule.h"
#include "../../Engine/Nullkiller.h"
#include "../../../../lib/pathfinder/CPathfinder.h"
#include "../../../../lib/pathfinder/TurnInfo.h"
namespace NKAI
{
@ -31,23 +33,79 @@ namespace AIPathfinding
if(!destination.blocked)
{
return;
if(source.node->layer == EPathfindingLayer::LAND
&& (destination.node->layer == EPathfindingLayer::AIR || destination.node->layer == EPathfindingLayer::WATER))
{
if(pathfinderHelper->getTurnInfo()->isLayerAvailable(destination.node->layer))
return;
else
destination.blocked = true;
}
else
{
return;
}
}
if(source.node->layer == EPathfindingLayer::LAND && destination.node->layer == EPathfindingLayer::SAIL)
{
std::shared_ptr<const VirtualBoatAction> virtualBoat = findVirtualBoat(destination, source);
if(virtualBoat && tryEmbarkVirtualBoat(destination, source, virtualBoat))
if(virtualBoat && tryUseSpecialAction(destination, source, virtualBoat, EPathNodeAction::EMBARK))
{
#if NKAI_PATHFINDER_TRACE_LEVEL >= 1
logAi->trace("Embarking to virtual boat while moving %s -> %s!", source.coord.toString(), destination.coord.toString());
#endif
}
}
if(source.node->layer == EPathfindingLayer::LAND && destination.node->layer == EPathfindingLayer::WATER)
{
auto action = waterWalkingActions.find(nodeStorage->getHero(source.node));
if(action != waterWalkingActions.end() && tryUseSpecialAction(destination, source, action->second, EPathNodeAction::NORMAL))
{
#if NKAI_PATHFINDER_TRACE_LEVEL >= 2
logAi->trace("Casting water walk while moving %s -> %s!", source.coord.toString(), destination.coord.toString());
#endif
}
}
if(source.node->layer == EPathfindingLayer::LAND && destination.node->layer == EPathfindingLayer::AIR)
{
auto action = airWalkingActions.find(nodeStorage->getHero(source.node));
if(action != airWalkingActions.end() && tryUseSpecialAction(destination, source, action->second, EPathNodeAction::NORMAL))
{
#if NKAI_PATHFINDER_TRACE_LEVEL >= 2
logAi->trace("Casting fly while moving %s -> %s!", source.coord.toString(), destination.coord.toString());
#endif
}
}
}
void AILayerTransitionRule::setup()
{
SpellID waterWalk = SpellID::WATER_WALK;
SpellID airWalk = SpellID::FLY;
for(const CGHeroInstance * hero : nodeStorage->getAllHeroes())
{
if(hero->canCastThisSpell(waterWalk.toSpell()))
{
waterWalkingActions[hero] = std::make_shared<WaterWalkingAction>(hero);
}
if(hero->canCastThisSpell(airWalk.toSpell()))
{
airWalkingActions[hero] = std::make_shared<AirWalkingAction>(hero);
}
}
collectVirtualBoats();
}
void AILayerTransitionRule::collectVirtualBoats()
{
std::vector<const IShipyard *> shipyards;
@ -81,7 +139,7 @@ namespace AIPathfinding
auto summonBoatSpell = SpellID(SpellID::SUMMON_BOAT).toSpell();
if(hero->canCastThisSpell(summonBoatSpell)
&& hero->getSpellSchoolLevel(summonBoatSpell) >= SecSkillLevel::ADVANCED)
&& hero->getSpellSchoolLevel(summonBoatSpell) >= MasteryLevel::ADVANCED)
{
// TODO: For lower school level we might need to check the existance of some boat
summonableVirtualBoats[hero] = std::make_shared<SummonBoatAction>();
@ -113,50 +171,56 @@ namespace AIPathfinding
return virtualBoat;
}
bool AILayerTransitionRule::tryEmbarkVirtualBoat(
bool AILayerTransitionRule::tryUseSpecialAction(
CDestinationNodeInfo & destination,
const PathNodeInfo & source,
std::shared_ptr<const VirtualBoatAction> virtualBoat) const
std::shared_ptr<const SpecialAction> specialAction,
EPathNodeAction targetAction) const
{
bool result = false;
nodeStorage->updateAINode(destination.node, [&](AIPathNode * node)
if(!specialAction->canAct(nodeStorage->getAINode(source.node)))
{
auto boatNodeOptional = nodeStorage->getOrCreateNode(
node->coord,
node->layer,
virtualBoat->getActor(node->actor));
return false;
}
if(boatNodeOptional)
nodeStorage->updateAINode(destination.node, [&](AIPathNode * node)
{
AIPathNode * boatNode = boatNodeOptional.value();
auto castNodeOptional = nodeStorage->getOrCreateNode(
node->coord,
node->layer,
specialAction->getActor(node->actor));
if(boatNode->action == EPathNodeAction::UNKNOWN)
if(castNodeOptional)
{
boatNode->addSpecialAction(virtualBoat);
destination.blocked = false;
destination.action = EPathNodeAction::EMBARK;
destination.node = boatNode;
result = true;
AIPathNode * castNode = castNodeOptional.value();
if(castNode->action == EPathNodeAction::UNKNOWN)
{
castNode->addSpecialAction(specialAction);
destination.blocked = false;
destination.action = targetAction;
destination.node = castNode;
result = true;
}
else
{
#if NKAI_PATHFINDER_TRACE_LEVEL >= 1
logAi->trace(
"Special transition node already allocated. Blocked moving %s -> %s",
source.coord.toString(),
destination.coord.toString());
#endif
}
}
else
{
#if NKAI_PATHFINDER_TRACE_LEVEL >= 1
logAi->trace(
"Special transition node already allocated. Blocked moving %s -> %s",
logAi->debug(
"Can not allocate special transition node while moving %s -> %s",
source.coord.toString(),
destination.coord.toString());
#endif
}
}
else
{
logAi->debug(
"Can not allocate special transition node while moving %s -> %s",
source.coord.toString(),
destination.coord.toString());
}
});
});
return result;
}

View File

@ -13,6 +13,7 @@
#include "../AINodeStorage.h"
#include "../../AIGateway.h"
#include "../Actions/BoatActions.h"
#include "../Actions/AdventureSpellCastMovementActions.h"
#include "../../../../CCallback.h"
#include "../../../../lib/mapObjects/MapObjects.h"
#include "../../../../lib/pathfinder/PathfindingRules.h"
@ -29,6 +30,8 @@ namespace AIPathfinding
std::map<int3, std::shared_ptr<const BuildBoatAction>> virtualBoats;
std::shared_ptr<AINodeStorage> nodeStorage;
std::map<const CGHeroInstance *, std::shared_ptr<const SummonBoatAction>> summonableVirtualBoats;
std::map<const CGHeroInstance *, std::shared_ptr<const WaterWalkingAction>> waterWalkingActions;
std::map<const CGHeroInstance *, std::shared_ptr<const AirWalkingAction>> airWalkingActions;
public:
AILayerTransitionRule(CPlayerSpecificInfoCallback * cb, Nullkiller * ai, std::shared_ptr<AINodeStorage> nodeStorage);
@ -41,15 +44,17 @@ namespace AIPathfinding
private:
void setup();
void collectVirtualBoats();
std::shared_ptr<const VirtualBoatAction> findVirtualBoat(
CDestinationNodeInfo & destination,
const PathNodeInfo & source) const;
bool tryEmbarkVirtualBoat(
bool tryUseSpecialAction(
CDestinationNodeInfo & destination,
const PathNodeInfo & source,
std::shared_ptr<const VirtualBoatAction> virtualBoat) const;
std::shared_ptr<const SpecialAction> specialAction,
EPathNodeAction targetAction) const;
};
}

View File

@ -130,7 +130,9 @@ namespace AIPathfinding
auto questInfo = QuestInfo(questObj->quest, destination.nodeObject, destination.coord);
QuestAction questAction(questInfo);
if(destination.nodeObject->ID == Obj::QUEST_GUARD && questObj->quest->missionType == CQuest::MISSION_NONE)
if(destination.nodeObject->ID == Obj::QUEST_GUARD
&& questObj->quest->mission == Rewardable::Limiter{}
&& questObj->quest->killTarget == ObjectInstanceID::NONE)
{
return false;
}

View File

@ -1,2 +1,2 @@
// Creates the precompiled header
// Creates the precompiled header
#include "StdInc.h"

View File

@ -1,9 +1,9 @@
#pragma once
#include "../../Global.h"
// This header should be treated as a pre compiled header file(PCH) in the compiler building settings.
// Here you can add specific libraries and macros which are specific to this project.
VCMI_LIB_USING_NAMESPACE
#pragma once
#include "../../Global.h"
// This header should be treated as a pre compiled header file(PCH) in the compiler building settings.
// Here you can add specific libraries and macros which are specific to this project.
VCMI_LIB_USING_NAMESPACE

View File

@ -1,324 +1,333 @@
/*
* StupidAI.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "../../lib/AI_Base.h"
#include "StupidAI.h"
#include "../../lib/CStack.h"
#include "../../CCallback.h"
#include "../../lib/CCreatureHandler.h"
static std::shared_ptr<CBattleCallback> cbc;
CStupidAI::CStupidAI()
: side(-1)
, wasWaitingForRealize(false)
, wasUnlockingGs(false)
{
print("created");
}
CStupidAI::~CStupidAI()
{
print("destroyed");
if(cb)
{
//Restore previous state of CB - it may be shared with the main AI (like VCAI)
cb->waitTillRealize = wasWaitingForRealize;
cb->unlockGsWhenWaiting = wasUnlockingGs;
}
}
void CStupidAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB)
{
print("init called, saving ptr to IBattleCallback");
env = ENV;
cbc = cb = CB;
wasWaitingForRealize = CB->waitTillRealize;
wasUnlockingGs = CB->unlockGsWhenWaiting;
CB->waitTillRealize = false;
CB->unlockGsWhenWaiting = false;
}
void CStupidAI::actionFinished(const BattleAction &action)
{
print("actionFinished called");
}
void CStupidAI::actionStarted(const BattleAction &action)
{
print("actionStarted called");
}
class EnemyInfo
{
public:
const CStack * s;
int adi, adr;
std::vector<BattleHex> attackFrom; //for melee fight
EnemyInfo(const CStack * _s) : s(_s), adi(0), adr(0)
{}
void calcDmg(const CStack * ourStack)
{
// FIXME: provide distance info for Jousting bonus
DamageEstimation retal;
DamageEstimation dmg = cbc->battleEstimateDamage(ourStack, s, 0, &retal);
adi = static_cast<int>((dmg.damage.min + dmg.damage.max) / 2);
adr = static_cast<int>((retal.damage.min + retal.damage.max) / 2);
}
bool operator==(const EnemyInfo& ei) const
{
return s == ei.s;
}
};
bool isMoreProfitable(const EnemyInfo &ei1, const EnemyInfo& ei2)
{
return (ei1.adi-ei1.adr) < (ei2.adi - ei2.adr);
}
static bool willSecondHexBlockMoreEnemyShooters(const BattleHex &h1, const BattleHex &h2)
{
int shooters[2] = {0}; //count of shooters on hexes
for(int i = 0; i < 2; i++)
{
for (auto & neighbour : (i ? h2 : h1).neighbouringTiles())
if(const auto * s = cbc->battleGetUnitByPos(neighbour))
if(s->isShooter())
shooters[i]++;
}
return shooters[0] < shooters[1];
}
void CStupidAI::yourTacticPhase(int distance)
{
cb->battleMakeTacticAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide()));
}
void CStupidAI::activeStack( const CStack * stack )
{
//boost::this_thread::sleep(boost::posix_time::seconds(2));
print("activeStack called for " + stack->nodeName());
ReachabilityInfo dists = cb->getReachability(stack);
std::vector<EnemyInfo> enemiesShootable, enemiesReachable, enemiesUnreachable;
if(stack->creatureId() == CreatureID::CATAPULT)
{
BattleAction attack;
static const std::vector<int> wallHexes = {50, 183, 182, 130, 78, 29, 12, 95};
auto seletectedHex = *RandomGeneratorUtil::nextItem(wallHexes, CRandomGenerator::getDefault());
attack.aimToHex(seletectedHex);
attack.actionType = EActionType::CATAPULT;
attack.side = side;
attack.stackNumber = stack->unitId();
cb->battleMakeUnitAction(attack);
return;
}
else if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON))
{
cb->battleMakeUnitAction(BattleAction::makeDefend(stack));
return;
}
for (const CStack *s : cb->battleGetStacks(CBattleCallback::ONLY_ENEMY))
{
if(cb->battleCanShoot(stack, s->getPosition()))
{
enemiesShootable.push_back(s);
}
else
{
std::vector<BattleHex> avHexes = cb->battleGetAvailableHexes(stack, false);
for (BattleHex hex : avHexes)
{
if(CStack::isMeleeAttackPossible(stack, s, hex))
{
std::vector<EnemyInfo>::iterator i = std::find(enemiesReachable.begin(), enemiesReachable.end(), s);
if(i == enemiesReachable.end())
{
enemiesReachable.push_back(s);
i = enemiesReachable.begin() + (enemiesReachable.size() - 1);
}
i->attackFrom.push_back(hex);
}
}
if(!vstd::contains(enemiesReachable, s) && s->getPosition().isValid())
enemiesUnreachable.push_back(s);
}
}
for ( auto & enemy : enemiesReachable )
enemy.calcDmg( stack );
for ( auto & enemy : enemiesShootable )
enemy.calcDmg( stack );
if(enemiesShootable.size())
{
const EnemyInfo &ei= *std::max_element(enemiesShootable.begin(), enemiesShootable.end(), isMoreProfitable);
cb->battleMakeUnitAction(BattleAction::makeShotAttack(stack, ei.s));
return;
}
else if(enemiesReachable.size())
{
const EnemyInfo &ei= *std::max_element(enemiesReachable.begin(), enemiesReachable.end(), &isMoreProfitable);
cb->battleMakeUnitAction(BattleAction::makeMeleeAttack(stack, ei.s->getPosition(), *std::max_element(ei.attackFrom.begin(), ei.attackFrom.end(), &willSecondHexBlockMoreEnemyShooters)));
return;
}
else if(enemiesUnreachable.size()) //due to #955 - a buggy battle may occur when there are no enemies
{
auto closestEnemy = vstd::minElementByFun(enemiesUnreachable, [&](const EnemyInfo & ei) -> int
{
return dists.distToNearestNeighbour(stack, ei.s);
});
if(dists.distToNearestNeighbour(stack, closestEnemy->s) < GameConstants::BFIELD_SIZE)
{
cb->battleMakeUnitAction(goTowards(stack, closestEnemy->s->getAttackableHexes(stack)));
return;
}
}
cb->battleMakeUnitAction(BattleAction::makeDefend(stack));
return;
}
void CStupidAI::battleAttack(const BattleAttack *ba)
{
print("battleAttack called");
}
void CStupidAI::battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged)
{
print("battleStacksAttacked called");
}
void CStupidAI::battleEnd(const BattleResult *br, QueryID queryID)
{
print("battleEnd called");
}
// void CStupidAI::battleResultsApplied()
// {
// print("battleResultsApplied called");
// }
void CStupidAI::battleNewRoundFirst(int round)
{
print("battleNewRoundFirst called");
}
void CStupidAI::battleNewRound(int round)
{
print("battleNewRound called");
}
void CStupidAI::battleStackMoved(const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport)
{
print("battleStackMoved called");
}
void CStupidAI::battleSpellCast(const BattleSpellCast *sc)
{
print("battleSpellCast called");
}
void CStupidAI::battleStacksEffectsSet(const SetStackEffect & sse)
{
print("battleStacksEffectsSet called");
}
void CStupidAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed)
{
print("battleStart called");
side = Side;
}
void CStupidAI::battleCatapultAttacked(const CatapultAttack & ca)
{
print("battleCatapultAttacked called");
}
void CStupidAI::print(const std::string &text) const
{
logAi->trace("CStupidAI [%p]: %s", this, text);
}
BattleAction CStupidAI::goTowards(const CStack * stack, std::vector<BattleHex> hexes) const
{
auto reachability = cb->getReachability(stack);
auto avHexes = cb->battleGetAvailableHexes(reachability, stack, false);
if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked
{
return BattleAction::makeDefend(stack);
}
std::sort(hexes.begin(), hexes.end(), [&](BattleHex h1, BattleHex h2) -> bool
{
return reachability.distances[h1] < reachability.distances[h2];
});
for(auto hex : hexes)
{
if(vstd::contains(avHexes, hex))
return BattleAction::makeMove(stack, hex);
if(stack->coversPos(hex))
{
logAi->warn("Warning: already standing on neighbouring tile!");
//We shouldn't even be here...
return BattleAction::makeDefend(stack);
}
}
BattleHex bestNeighbor = hexes.front();
if(reachability.distances[bestNeighbor] > GameConstants::BFIELD_SIZE)
{
return BattleAction::makeDefend(stack);
}
if(stack->hasBonusOfType(BonusType::FLYING))
{
// Flying stack doesn't go hex by hex, so we can't backtrack using predecessors.
// We just check all available hexes and pick the one closest to the target.
auto nearestAvailableHex = vstd::minElementByFun(avHexes, [&](BattleHex hex) -> int
{
return BattleHex::getDistance(bestNeighbor, hex);
});
return BattleAction::makeMove(stack, *nearestAvailableHex);
}
else
{
BattleHex currentDest = bestNeighbor;
while(1)
{
if(!currentDest.isValid())
{
logAi->error("CBattleAI::goTowards: internal error");
return BattleAction::makeDefend(stack);
}
if(vstd::contains(avHexes, currentDest))
return BattleAction::makeMove(stack, currentDest);
currentDest = reachability.predecessors[currentDest];
}
}
}
/*
* StupidAI.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "../../lib/AI_Base.h"
#include "StupidAI.h"
#include "../../lib/CStack.h"
#include "../../CCallback.h"
#include "../../lib/CCreatureHandler.h"
#include "../../lib/battle/BattleAction.h"
#include "../../lib/battle/BattleInfo.h"
static std::shared_ptr<CBattleCallback> cbc;
CStupidAI::CStupidAI()
: side(-1)
, wasWaitingForRealize(false)
, wasUnlockingGs(false)
{
print("created");
}
CStupidAI::~CStupidAI()
{
print("destroyed");
if(cb)
{
//Restore previous state of CB - it may be shared with the main AI (like VCAI)
cb->waitTillRealize = wasWaitingForRealize;
cb->unlockGsWhenWaiting = wasUnlockingGs;
}
}
void CStupidAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB)
{
print("init called, saving ptr to IBattleCallback");
env = ENV;
cbc = cb = CB;
wasWaitingForRealize = CB->waitTillRealize;
wasUnlockingGs = CB->unlockGsWhenWaiting;
CB->waitTillRealize = false;
CB->unlockGsWhenWaiting = false;
}
void CStupidAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB, AutocombatPreferences autocombatPreferences)
{
initBattleInterface(ENV, CB);
}
void CStupidAI::actionFinished(const BattleID & battleID, const BattleAction &action)
{
print("actionFinished called");
}
void CStupidAI::actionStarted(const BattleID & battleID, const BattleAction &action)
{
print("actionStarted called");
}
class EnemyInfo
{
public:
const CStack * s;
int adi, adr;
std::vector<BattleHex> attackFrom; //for melee fight
EnemyInfo(const CStack * _s) : s(_s), adi(0), adr(0)
{}
void calcDmg(const BattleID & battleID, const CStack * ourStack)
{
// FIXME: provide distance info for Jousting bonus
DamageEstimation retal;
DamageEstimation dmg = cbc->getBattle(battleID)->battleEstimateDamage(ourStack, s, 0, &retal);
adi = static_cast<int>((dmg.damage.min + dmg.damage.max) / 2);
adr = static_cast<int>((retal.damage.min + retal.damage.max) / 2);
}
bool operator==(const EnemyInfo& ei) const
{
return s == ei.s;
}
};
bool isMoreProfitable(const EnemyInfo &ei1, const EnemyInfo& ei2)
{
return (ei1.adi-ei1.adr) < (ei2.adi - ei2.adr);
}
static bool willSecondHexBlockMoreEnemyShooters(const BattleID & battleID, const BattleHex &h1, const BattleHex &h2)
{
int shooters[2] = {0}; //count of shooters on hexes
for(int i = 0; i < 2; i++)
{
for (auto & neighbour : (i ? h2 : h1).neighbouringTiles())
if(const auto * s = cbc->getBattle(battleID)->battleGetUnitByPos(neighbour))
if(s->isShooter())
shooters[i]++;
}
return shooters[0] < shooters[1];
}
void CStupidAI::yourTacticPhase(const BattleID & battleID, int distance)
{
cb->battleMakeTacticAction(battleID, BattleAction::makeEndOFTacticPhase(cb->getBattle(battleID)->battleGetTacticsSide()));
}
void CStupidAI::activeStack(const BattleID & battleID, const CStack * stack)
{
//boost::this_thread::sleep_for(boost::chrono::seconds(2));
print("activeStack called for " + stack->nodeName());
ReachabilityInfo dists = cb->getBattle(battleID)->getReachability(stack);
std::vector<EnemyInfo> enemiesShootable, enemiesReachable, enemiesUnreachable;
if(stack->creatureId() == CreatureID::CATAPULT)
{
BattleAction attack;
static const std::vector<int> wallHexes = {50, 183, 182, 130, 78, 29, 12, 95};
auto seletectedHex = *RandomGeneratorUtil::nextItem(wallHexes, CRandomGenerator::getDefault());
attack.aimToHex(seletectedHex);
attack.actionType = EActionType::CATAPULT;
attack.side = side;
attack.stackNumber = stack->unitId();
cb->battleMakeUnitAction(battleID, attack);
return;
}
else if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON))
{
cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack));
return;
}
for (const CStack *s : cb->getBattle(battleID)->battleGetStacks(CBattleInfoEssentials::ONLY_ENEMY))
{
if(cb->getBattle(battleID)->battleCanShoot(stack, s->getPosition()))
{
enemiesShootable.push_back(s);
}
else
{
std::vector<BattleHex> avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(stack, false);
for (BattleHex hex : avHexes)
{
if(CStack::isMeleeAttackPossible(stack, s, hex))
{
std::vector<EnemyInfo>::iterator i = std::find(enemiesReachable.begin(), enemiesReachable.end(), s);
if(i == enemiesReachable.end())
{
enemiesReachable.push_back(s);
i = enemiesReachable.begin() + (enemiesReachable.size() - 1);
}
i->attackFrom.push_back(hex);
}
}
if(!vstd::contains(enemiesReachable, s) && s->getPosition().isValid())
enemiesUnreachable.push_back(s);
}
}
for ( auto & enemy : enemiesReachable )
enemy.calcDmg(battleID, stack);
for ( auto & enemy : enemiesShootable )
enemy.calcDmg(battleID, stack);
if(enemiesShootable.size())
{
const EnemyInfo &ei= *std::max_element(enemiesShootable.begin(), enemiesShootable.end(), isMoreProfitable);
cb->battleMakeUnitAction(battleID, BattleAction::makeShotAttack(stack, ei.s));
return;
}
else if(enemiesReachable.size())
{
const EnemyInfo &ei= *std::max_element(enemiesReachable.begin(), enemiesReachable.end(), &isMoreProfitable);
BattleHex targetHex = *std::max_element(ei.attackFrom.begin(), ei.attackFrom.end(), [&](auto a, auto b) { return willSecondHexBlockMoreEnemyShooters(battleID, a, b);});
cb->battleMakeUnitAction(battleID, BattleAction::makeMeleeAttack(stack, ei.s->getPosition(), targetHex));
return;
}
else if(enemiesUnreachable.size()) //due to #955 - a buggy battle may occur when there are no enemies
{
auto closestEnemy = vstd::minElementByFun(enemiesUnreachable, [&](const EnemyInfo & ei) -> int
{
return dists.distToNearestNeighbour(stack, ei.s);
});
if(dists.distToNearestNeighbour(stack, closestEnemy->s) < GameConstants::BFIELD_SIZE)
{
cb->battleMakeUnitAction(battleID, goTowards(battleID, stack, closestEnemy->s->getAttackableHexes(stack)));
return;
}
}
cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack));
return;
}
void CStupidAI::battleAttack(const BattleID & battleID, const BattleAttack *ba)
{
print("battleAttack called");
}
void CStupidAI::battleStacksAttacked(const BattleID & battleID, const std::vector<BattleStackAttacked> & bsa, bool ranged)
{
print("battleStacksAttacked called");
}
void CStupidAI::battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID)
{
print("battleEnd called");
}
// void CStupidAI::battleResultsApplied()
// {
// print("battleResultsApplied called");
// }
void CStupidAI::battleNewRoundFirst(const BattleID & battleID)
{
print("battleNewRoundFirst called");
}
void CStupidAI::battleNewRound(const BattleID & battleID)
{
print("battleNewRound called");
}
void CStupidAI::battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport)
{
print("battleStackMoved called");
}
void CStupidAI::battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc)
{
print("battleSpellCast called");
}
void CStupidAI::battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse)
{
print("battleStacksEffectsSet called");
}
void CStupidAI::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed)
{
print("battleStart called");
side = Side;
}
void CStupidAI::battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca)
{
print("battleCatapultAttacked called");
}
void CStupidAI::print(const std::string &text) const
{
logAi->trace("CStupidAI [%p]: %s", this, text);
}
BattleAction CStupidAI::goTowards(const BattleID & battleID, const CStack * stack, std::vector<BattleHex> hexes) const
{
auto reachability = cb->getBattle(battleID)->getReachability(stack);
auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false);
if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked
{
return BattleAction::makeDefend(stack);
}
std::sort(hexes.begin(), hexes.end(), [&](BattleHex h1, BattleHex h2) -> bool
{
return reachability.distances[h1] < reachability.distances[h2];
});
for(auto hex : hexes)
{
if(vstd::contains(avHexes, hex))
return BattleAction::makeMove(stack, hex);
if(stack->coversPos(hex))
{
logAi->warn("Warning: already standing on neighbouring tile!");
//We shouldn't even be here...
return BattleAction::makeDefend(stack);
}
}
BattleHex bestNeighbor = hexes.front();
if(reachability.distances[bestNeighbor] > GameConstants::BFIELD_SIZE)
{
return BattleAction::makeDefend(stack);
}
if(stack->hasBonusOfType(BonusType::FLYING))
{
// Flying stack doesn't go hex by hex, so we can't backtrack using predecessors.
// We just check all available hexes and pick the one closest to the target.
auto nearestAvailableHex = vstd::minElementByFun(avHexes, [&](BattleHex hex) -> int
{
return BattleHex::getDistance(bestNeighbor, hex);
});
return BattleAction::makeMove(stack, *nearestAvailableHex);
}
else
{
BattleHex currentDest = bestNeighbor;
while(1)
{
if(!currentDest.isValid())
{
logAi->error("CBattleAI::goTowards: internal error");
return BattleAction::makeDefend(stack);
}
if(vstd::contains(avHexes, currentDest))
return BattleAction::makeMove(stack, currentDest);
currentDest = reachability.predecessors[currentDest];
}
}
}

View File

@ -1,53 +1,56 @@
/*
* StupidAI.h, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#pragma once
#include "../../lib/battle/BattleHex.h"
#include "../../lib/battle/ReachabilityInfo.h"
class EnemyInfo;
class CStupidAI : public CBattleGameInterface
{
int side;
std::shared_ptr<CBattleCallback> cb;
std::shared_ptr<Environment> env;
bool wasWaitingForRealize;
bool wasUnlockingGs;
void print(const std::string &text) const;
public:
CStupidAI();
~CStupidAI();
void initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB) override;
void actionFinished(const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero
void actionStarted(const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero
void activeStack(const CStack * stack) override; //called when it's turn of that stack
void yourTacticPhase(int distance) override;
void battleAttack(const BattleAttack *ba) override; //called when stack is performing attack
void battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged) override; //called when stack receives damage (after battleAttack())
void battleEnd(const BattleResult *br, QueryID queryID) override;
//void battleResultsApplied() override; //called when all effects of last battle are applied
void battleNewRoundFirst(int round) override; //called at the beginning of each turn before changes are applied;
void battleNewRound(int round) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn
void battleStackMoved(const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport) override;
void battleSpellCast(const BattleSpellCast *sc) override;
void battleStacksEffectsSet(const SetStackEffect & sse) override;//called when a specific effect is set to stacks
//void battleTriggerEffect(const BattleTriggerEffect & bte) override;
void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right
void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack
private:
BattleAction goTowards(const CStack * stack, std::vector<BattleHex> hexes) const;
};
/*
* StupidAI.h, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#pragma once
#include "../../lib/battle/BattleHex.h"
#include "../../lib/battle/ReachabilityInfo.h"
#include "../../lib/CGameInterface.h"
class EnemyInfo;
class CStupidAI : public CBattleGameInterface
{
int side;
std::shared_ptr<CBattleCallback> cb;
std::shared_ptr<Environment> env;
bool wasWaitingForRealize;
bool wasUnlockingGs;
void print(const std::string &text) const;
public:
CStupidAI();
~CStupidAI();
void initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB) override;
void initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB, AutocombatPreferences autocombatPreferences) override;
void actionFinished(const BattleID & battleID, const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero
void actionStarted(const BattleID & battleID, const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero
void activeStack(const BattleID & battleID, const CStack * stack) override; //called when it's turn of that stack
void yourTacticPhase(const BattleID & battleID, int distance) override;
void battleAttack(const BattleID & battleID, const BattleAttack *ba) override; //called when stack is performing attack
void battleStacksAttacked(const BattleID & battleID, const std::vector<BattleStackAttacked> & bsa, bool ranged) override; //called when stack receives damage (after battleAttack())
void battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID) override;
//void battleResultsApplied() override; //called when all effects of last battle are applied
void battleNewRoundFirst(const BattleID & battleID) override; //called at the beginning of each turn before changes are applied;
void battleNewRound(const BattleID & battleID) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn
void battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport) override;
void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) override;
void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) override;//called when a specific effect is set to stacks
//void battleTriggerEffect(const BattleTriggerEffect & bte) override;
void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right
void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override; //called when catapult makes an attack
private:
BattleAction goTowards(const BattleID & battleID, const CStack * stack, std::vector<BattleHex> hexes) const;
};

View File

@ -1,34 +1,34 @@
/*
* main.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "../../lib/AI_Base.h"
#include "StupidAI.h"
#ifdef __GNUC__
#define strcpy_s(a, b, c) strncpy(a, c, b)
#endif
static const char *g_cszAiName = "Stupid AI 0.1";
extern "C" DLL_EXPORT int GetGlobalAiVersion()
{
return AI_INTERFACE_VER;
}
extern "C" DLL_EXPORT void GetAiName(char* name)
{
strcpy_s(name, strlen(g_cszAiName) + 1, g_cszAiName);
}
extern "C" DLL_EXPORT void GetNewBattleAI(std::shared_ptr<CBattleGameInterface> &out)
{
out = std::make_shared<CStupidAI>();
}
/*
* main.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "../../lib/AI_Base.h"
#include "StupidAI.h"
#ifdef __GNUC__
#define strcpy_s(a, b, c) strncpy(a, c, b)
#endif
static const char *g_cszAiName = "Stupid AI 0.1";
extern "C" DLL_EXPORT int GetGlobalAiVersion()
{
return AI_INTERFACE_VER;
}
extern "C" DLL_EXPORT void GetAiName(char* name)
{
strcpy_s(name, strlen(g_cszAiName) + 1, g_cszAiName);
}
extern "C" DLL_EXPORT void GetNewBattleAI(std::shared_ptr<CBattleGameInterface> &out)
{
out = std::make_shared<CStupidAI>();
}

View File

@ -1,263 +1,259 @@
/*
* AIUtility.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "AIUtility.h"
#include "VCAI.h"
#include "FuzzyHelper.h"
#include "Goals/Goals.h"
#include "../../lib/UnlockGuard.h"
#include "../../lib/CConfigHandler.h"
#include "../../lib/CHeroHandler.h"
#include "../../lib/mapObjects/CBank.h"
#include "../../lib/mapObjects/CGTownInstance.h"
#include "../../lib/mapObjects/CQuest.h"
#include "../../lib/mapping/CMapDefines.h"
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<VCAI> ai;
extern FuzzyHelper * fh;
//extern static const int3 dirs[8];
const CGObjectInstance * ObjectIdRef::operator->() const
{
return cb->getObj(id, false);
}
ObjectIdRef::operator const CGObjectInstance *() const
{
return cb->getObj(id, false);
}
ObjectIdRef::operator bool() const
{
return cb->getObj(id, false);
}
ObjectIdRef::ObjectIdRef(ObjectInstanceID _id)
: id(_id)
{
}
ObjectIdRef::ObjectIdRef(const CGObjectInstance * obj)
: id(obj->id)
{
}
bool ObjectIdRef::operator<(const ObjectIdRef & rhs) const
{
return id < rhs.id;
}
HeroPtr::HeroPtr(const CGHeroInstance * H)
{
if(!H)
{
//init from nullptr should equal to default init
*this = HeroPtr();
return;
}
h = H;
name = h->getNameTranslated();
hid = H->id;
// infosCount[ai->playerID][hid]++;
}
HeroPtr::HeroPtr()
{
h = nullptr;
hid = ObjectInstanceID();
}
HeroPtr::~HeroPtr()
{
// if(hid >= 0)
// infosCount[ai->playerID][hid]--;
}
bool HeroPtr::operator<(const HeroPtr & rhs) const
{
return hid < rhs.hid;
}
const CGHeroInstance * HeroPtr::get(bool doWeExpectNull) const
{
//TODO? check if these all assertions every time we get info about hero affect efficiency
//
//behave terribly when attempting unauthorized access to hero that is not ours (or was lost)
assert(doWeExpectNull || h);
if(h)
{
auto obj = cb->getObj(hid);
const bool owned = obj && obj->tempOwner == ai->playerID;
if(doWeExpectNull && !owned)
{
return nullptr;
}
else
{
assert(obj);
assert(owned);
}
}
return h;
}
const CGHeroInstance * HeroPtr::operator->() const
{
return get();
}
bool HeroPtr::validAndSet() const
{
return get(true);
}
const CGHeroInstance * HeroPtr::operator*() const
{
return get();
}
bool HeroPtr::operator==(const HeroPtr & rhs) const
{
return h == rhs.get(true);
}
bool CDistanceSorter::operator()(const CGObjectInstance * lhs, const CGObjectInstance * rhs) const
{
const CGPathNode * ln = ai->myCb->getPathsInfo(hero)->getPathInfo(lhs->visitablePos());
const CGPathNode * rn = ai->myCb->getPathsInfo(hero)->getPathInfo(rhs->visitablePos());
return ln->getCost() < rn->getCost();
}
bool isSafeToVisit(HeroPtr h, crint3 tile)
{
return isSafeToVisit(h, fh->evaluateDanger(tile, h.get()));
}
bool isSafeToVisit(HeroPtr h, uint64_t dangerStrength)
{
const ui64 heroStrength = h->getTotalStrength();
if(dangerStrength)
{
return heroStrength / SAFE_ATTACK_CONSTANT > dangerStrength;
}
return true; //there's no danger
}
bool isObjectRemovable(const CGObjectInstance * obj)
{
//FIXME: move logic to object property!
switch (obj->ID)
{
case Obj::MONSTER:
case Obj::RESOURCE:
case Obj::CAMPFIRE:
case Obj::TREASURE_CHEST:
case Obj::ARTIFACT:
case Obj::BORDERGUARD:
case Obj::FLOTSAM:
case Obj::PANDORAS_BOX:
case Obj::OCEAN_BOTTLE:
case Obj::SEA_CHEST:
case Obj::SHIPWRECK_SURVIVOR:
case Obj::SPELL_SCROLL:
return true;
break;
default:
return false;
break;
}
}
bool canBeEmbarkmentPoint(const TerrainTile * t, bool fromWater)
{
// TODO: Such information should be provided by pathfinder
// Tile must be free or with unoccupied boat
if(!t->blocked)
{
return true;
}
else if(!fromWater) // do not try to board when in water sector
{
if(t->visitableObjects.size() == 1 && t->topVisitableId() == Obj::BOAT)
return true;
}
return false;
}
bool isBlockedBorderGate(int3 tileToHit) //TODO: is that function needed? should be handled by pathfinder
{
if(cb->getTile(tileToHit)->topVisitableId() != Obj::BORDER_GATE)
return false;
auto gate = dynamic_cast<const CGKeys *>(cb->getTile(tileToHit)->topVisitableObj());
return !gate->passableFor(ai->playerID);
}
bool isBlockVisitObj(const int3 & pos)
{
if(auto obj = cb->getTopObj(pos))
{
if(obj->isBlockedVisitable()) //we can't stand on that object
return true;
}
return false;
}
creInfo infoFromDC(const dwellingContent & dc)
{
creInfo ci;
ci.count = dc.first;
ci.creID = dc.second.size() ? dc.second.back() : CreatureID(-1); //should never be accessed
if (ci.creID != -1)
{
ci.cre = VLC->creatures()->getById(ci.creID);
ci.level = ci.cre->getLevel(); //this is creature tier, while tryRealize expects dwelling level. Ignore.
}
else
{
ci.cre = nullptr;
ci.level = 0;
}
return ci;
}
bool compareHeroStrength(HeroPtr h1, HeroPtr h2)
{
return h1->getTotalStrength() < h2->getTotalStrength();
}
bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2)
{
return a1->getArmyStrength() < a2->getArmyStrength();
}
bool compareArtifacts(const CArtifactInstance * a1, const CArtifactInstance * a2)
{
auto art1 = a1->artType;
auto art2 = a2->artType;
if(art1->getPrice() == art2->getPrice())
return art1->valOfBonuses(BonusType::PRIMARY_SKILL) > art2->valOfBonuses(BonusType::PRIMARY_SKILL);
else
return art1->getPrice() > art2->getPrice();
}
/*
* AIUtility.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "AIUtility.h"
#include "VCAI.h"
#include "FuzzyHelper.h"
#include "Goals/Goals.h"
#include "../../lib/UnlockGuard.h"
#include "../../lib/CConfigHandler.h"
#include "../../lib/CHeroHandler.h"
#include "../../lib/mapObjects/CBank.h"
#include "../../lib/mapObjects/CGTownInstance.h"
#include "../../lib/mapObjects/CQuest.h"
#include "../../lib/mapping/CMapDefines.h"
extern FuzzyHelper * fh;
const CGObjectInstance * ObjectIdRef::operator->() const
{
return cb->getObj(id, false);
}
ObjectIdRef::operator const CGObjectInstance *() const
{
return cb->getObj(id, false);
}
ObjectIdRef::operator bool() const
{
return cb->getObj(id, false);
}
ObjectIdRef::ObjectIdRef(ObjectInstanceID _id)
: id(_id)
{
}
ObjectIdRef::ObjectIdRef(const CGObjectInstance * obj)
: id(obj->id)
{
}
bool ObjectIdRef::operator<(const ObjectIdRef & rhs) const
{
return id < rhs.id;
}
HeroPtr::HeroPtr(const CGHeroInstance * H)
{
if(!H)
{
//init from nullptr should equal to default init
*this = HeroPtr();
return;
}
h = H;
name = h->getNameTranslated();
hid = H->id;
// infosCount[ai->playerID][hid]++;
}
HeroPtr::HeroPtr()
{
h = nullptr;
hid = ObjectInstanceID();
}
HeroPtr::~HeroPtr()
{
// if(hid >= 0)
// infosCount[ai->playerID][hid]--;
}
bool HeroPtr::operator<(const HeroPtr & rhs) const
{
return hid < rhs.hid;
}
const CGHeroInstance * HeroPtr::get(bool doWeExpectNull) const
{
//TODO? check if these all assertions every time we get info about hero affect efficiency
//
//behave terribly when attempting unauthorized access to hero that is not ours (or was lost)
assert(doWeExpectNull || h);
if(h)
{
auto obj = cb->getObj(hid);
const bool owned = obj && obj->tempOwner == ai->playerID;
if(doWeExpectNull && !owned)
{
return nullptr;
}
else
{
assert(obj);
assert(owned);
}
}
return h;
}
const CGHeroInstance * HeroPtr::operator->() const
{
return get();
}
bool HeroPtr::validAndSet() const
{
return get(true);
}
const CGHeroInstance * HeroPtr::operator*() const
{
return get();
}
bool HeroPtr::operator==(const HeroPtr & rhs) const
{
return h == rhs.get(true);
}
bool CDistanceSorter::operator()(const CGObjectInstance * lhs, const CGObjectInstance * rhs) const
{
const CGPathNode * ln = ai->myCb->getPathsInfo(hero)->getPathInfo(lhs->visitablePos());
const CGPathNode * rn = ai->myCb->getPathsInfo(hero)->getPathInfo(rhs->visitablePos());
return ln->getCost() < rn->getCost();
}
bool isSafeToVisit(HeroPtr h, crint3 tile)
{
return isSafeToVisit(h, fh->evaluateDanger(tile, h.get()));
}
bool isSafeToVisit(HeroPtr h, uint64_t dangerStrength)
{
const ui64 heroStrength = h->getTotalStrength();
if(dangerStrength)
{
return heroStrength / SAFE_ATTACK_CONSTANT > dangerStrength;
}
return true; //there's no danger
}
bool isObjectRemovable(const CGObjectInstance * obj)
{
//FIXME: move logic to object property!
switch (obj->ID)
{
case Obj::MONSTER:
case Obj::RESOURCE:
case Obj::CAMPFIRE:
case Obj::TREASURE_CHEST:
case Obj::ARTIFACT:
case Obj::BORDERGUARD:
case Obj::FLOTSAM:
case Obj::PANDORAS_BOX:
case Obj::OCEAN_BOTTLE:
case Obj::SEA_CHEST:
case Obj::SHIPWRECK_SURVIVOR:
case Obj::SPELL_SCROLL:
return true;
break;
default:
return false;
break;
}
}
bool canBeEmbarkmentPoint(const TerrainTile * t, bool fromWater)
{
// TODO: Such information should be provided by pathfinder
// Tile must be free or with unoccupied boat
if(!t->blocked)
{
return true;
}
else if(!fromWater) // do not try to board when in water sector
{
if(t->visitableObjects.size() == 1 && t->topVisitableId() == Obj::BOAT)
return true;
}
return false;
}
bool isBlockedBorderGate(int3 tileToHit) //TODO: is that function needed? should be handled by pathfinder
{
if(cb->getTile(tileToHit)->topVisitableId() != Obj::BORDER_GATE)
return false;
auto gate = dynamic_cast<const CGKeys *>(cb->getTile(tileToHit)->topVisitableObj());
return !gate->passableFor(ai->playerID);
}
bool isBlockVisitObj(const int3 & pos)
{
if(auto obj = cb->getTopObj(pos))
{
if(obj->isBlockedVisitable()) //we can't stand on that object
return true;
}
return false;
}
creInfo infoFromDC(const dwellingContent & dc)
{
creInfo ci;
ci.count = dc.first;
ci.creID = dc.second.size() ? dc.second.back() : CreatureID(-1); //should never be accessed
if (ci.creID != CreatureID::NONE)
{
ci.cre = VLC->creatures()->getById(ci.creID);
ci.level = ci.cre->getLevel(); //this is creature tier, while tryRealize expects dwelling level. Ignore.
}
else
{
ci.cre = nullptr;
ci.level = 0;
}
return ci;
}
bool compareHeroStrength(HeroPtr h1, HeroPtr h2)
{
return h1->getTotalStrength() < h2->getTotalStrength();
}
bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2)
{
return a1->getArmyStrength() < a2->getArmyStrength();
}
bool compareArtifacts(const CArtifactInstance * a1, const CArtifactInstance * a2)
{
auto art1 = a1->artType;
auto art2 = a2->artType;
if(art1->getPrice() == art2->getPrice())
return art1->valOfBonuses(BonusType::PRIMARY_SKILL) > art2->valOfBonuses(BonusType::PRIMARY_SKILL);
else
return art1->getPrice() > art2->getPrice();
}

View File

@ -18,6 +18,7 @@
#include "../../lib/mapObjects/CGHeroInstance.h"
#include "../../CCallback.h"
class VCAI;
class CCallback;
struct creInfo;
@ -33,7 +34,8 @@ const int ALLOWED_ROAMING_HEROES = 8;
extern const double SAFE_ATTACK_CONSTANT;
extern const int GOLD_RESERVE;
extern boost::thread_specific_ptr<CCallback> cb;
extern thread_local CCallback * cb;
extern thread_local VCAI * ai;
//provisional class for AI to store a reference to an owned hero object
//checks if it's valid on access, should be used in place of const CGHeroInstance*
@ -140,7 +142,7 @@ class ObjsVector : public std::vector<ObjectIdRef>
{
};
template<int id>
template<Obj::Type id>
bool objWithID(const CGObjectInstance * obj)
{
return obj->ID == id;
@ -192,7 +194,7 @@ void foreach_tile_pos(CCallback * cbp, const Func & foo) // avoid costly retriev
template<class Func>
void foreach_neighbour(const int3 & pos, const Func & foo)
{
CCallback * cbp = cb.get(); // avoid costly retrieval of thread-specific pointer
CCallback * cbp = cb; // avoid costly retrieval of thread-specific pointer
for(const int3 & dir : int3::getDirs())
{
const int3 n = pos + dir;

View File

@ -118,12 +118,12 @@ ui64 ArmyManager::howManyReinforcementsCanBuy(const CCreatureSet * h, const CGDw
{
creInfo ci = infoFromDC(dc);
if(!ci.count || ci.creID == -1)
if(!ci.count || ci.creID == CreatureID::NONE)
continue;
vstd::amin(ci.count, availableRes / ci.cre->getFullRecruitCost()); //max count we can afford
if(ci.count && ci.creID != -1) //valid creature at this level
if(ci.count && ci.creID != CreatureID::NONE) //valid creature at this level
{
//can be merged with another stack?
SlotID dst = h->getSlotFor(ci.creID);

View File

@ -38,7 +38,7 @@ bool BuildingManager::tryBuildThisStructure(const CGTownInstance * t, BuildingID
for (BuildingID buildID : toBuild)
{
EBuildingState::EBuildingState canBuild = cb->canBuildStructure(t, buildID);
EBuildingState canBuild = cb->canBuildStructure(t, buildID);
if (canBuild == EBuildingState::HAVE_CAPITAL || canBuild == EBuildingState::FORBIDDEN || canBuild == EBuildingState::NO_WATER)
return false; //we won't be able to build this
}
@ -52,7 +52,7 @@ bool BuildingManager::tryBuildThisStructure(const CGTownInstance * t, BuildingID
{
const CBuilding * b = t->town->buildings.at(buildID);
EBuildingState::EBuildingState canBuild = cb->canBuildStructure(t, buildID);
EBuildingState canBuild = cb->canBuildStructure(t, buildID);
if (canBuild == EBuildingState::ALLOWED)
{
PotentialBuilding pb;
@ -222,7 +222,7 @@ bool BuildingManager::getBuildingOptions(const CGTownInstance * t)
std::vector<BuildingID> extraBuildings;
for (auto buildingInfo : t->town->buildings)
{
if (buildingInfo.first > 43)
if (buildingInfo.first > BuildingID::DWELL_UP2_FIRST)
extraBuildings.push_back(buildingInfo.first);
}
return tryBuildAnyStructure(t, extraBuildings);

View File

@ -18,9 +18,6 @@
#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
extern boost::thread_specific_ptr<VCAI> ai;
extern FuzzyHelper * fh;
engineBase::engineBase()
{
rules = new fl::RuleBlock();

View File

@ -8,7 +8,11 @@
*
*/
#pragma once
#include <fl/Headers.h>
#if __has_include(<fuzzylite/Headers.h>)
# include <fuzzylite/Headers.h>
#else
# include <fl/Headers.h>
#endif
#include "Goals/AbstractGoal.h"
VCMI_LIB_NAMESPACE_BEGIN

View File

@ -23,9 +23,6 @@
FuzzyHelper * fh;
extern boost::thread_specific_ptr<VCAI> ai;
extern boost::thread_specific_ptr<CCallback> cb;
Goals::TSubgoal FuzzyHelper::chooseSolution(Goals::TGoalVec vec)
{
if(vec.empty())
@ -216,7 +213,7 @@ void FuzzyHelper::setPriority(Goals::TSubgoal & g) //calls evaluate - Visitor pa
ui64 FuzzyHelper::evaluateDanger(crint3 tile, const CGHeroInstance * visitor)
{
return evaluateDanger(tile, visitor, ai.get());
return evaluateDanger(tile, visitor, ai);
}
ui64 FuzzyHelper::evaluateDanger(crint3 tile, const CGHeroInstance * visitor, const VCAI * ai)
@ -285,7 +282,7 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj, const VCAI * ai)
{
auto cb = ai->myCb;
if(obj->tempOwner < PlayerColor::PLAYER_LIMIT && cb->getPlayerRelations(obj->tempOwner, ai->playerID) != PlayerRelations::ENEMIES) //owned or allied objects don't pose any threat
if(obj->tempOwner.isValidPlayer() && cb->getPlayerRelations(obj->tempOwner, ai->playerID) != PlayerRelations::ENEMIES) //owned or allied objects don't pose any threat
return 0;
switch(obj->ID)

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