1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-04-21 12:06:49 +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 test: 0
pack: 1 pack: 1
extension: ipa extension: ipa
preset: ios-release-conan preset: ios-release-conan-ccache
conan_profile: ios-arm64 conan_profile: ios-arm64
conan_options: --options with_apple_system_libs=True conan_options: --options with_apple_system_libs=True
- platform: msvc - platform: msvc
@ -111,7 +111,7 @@ jobs:
test: 0 test: 0
pack: 1 pack: 1
extension: exe extension: exe
preset: windows-msvc-release preset: windows-msvc-release-ccache
- platform: mingw-ubuntu - platform: mingw-ubuntu
os: ubuntu-22.04 os: ubuntu-22.04
test: 0 test: 0
@ -145,11 +145,27 @@ jobs:
with: with:
submodules: recursive 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 - name: Dependencies
run: source '${{github.workspace}}/CI/${{matrix.platform}}/before_install.sh' run: source '${{github.workspace}}/CI/${{matrix.platform}}/before_install.sh'
env: env:
VCMI_BUILD_PLATFORM: x64 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 - uses: actions/setup-python@v4
if: "${{ matrix.conan_profile != '' }}" if: "${{ matrix.conan_profile != '' }}"
with: with:
@ -185,9 +201,9 @@ jobs:
env: env:
PULL_REQUEST: ${{ github.event.pull_request.number }} PULL_REQUEST: ${{ github.event.pull_request.number }}
- name: CMake Preset - name: CMake Preset with ccache
run: | run: |
cmake --preset ${{ matrix.preset }} cmake -DCMAKE_CXX_COMPILER_LAUNCHER=ccache --preset ${{ matrix.preset }}
- name: Build Preset - name: Build Preset
run: | run: |

View File

@ -18,17 +18,98 @@ uint64_t averageDmg(const DamageRange & range)
return (range.min + range.max) / 2; 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) AttackPossibility::AttackPossibility(BattleHex from, BattleHex dest, const BattleAttackInfo & attack)
: from(from), dest(dest), attack(attack) : from(from), dest(dest), attack(attack)
{ {
} }
int64_t AttackPossibility::damageDiff() const float AttackPossibility::damageDiff() const
{ {
return defenderDamageReduce - attackerDamageReduce - collateralDamageReduce + shootersBlockedDmg; 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(); return damageDiff();
} }
@ -38,25 +119,28 @@ int64_t AttackPossibility::attackValue() const
/// Half bounty for kill, half for making damage equal to enemy health /// Half bounty for kill, half for making damage equal to enemy health
/// Bounty - the killed creature average damage calculated against attacker /// Bounty - the killed creature average damage calculated against attacker
/// </summary> /// </summary>
int64_t AttackPossibility::calculateDamageReduce( float AttackPossibility::calculateDamageReduce(
const battle::Unit * attacker, const battle::Unit * attacker,
const battle::Unit * defender, const battle::Unit * defender,
uint64_t damageDealt, uint64_t damageDealt,
const CBattleInfoCallback & cb) DamageCache & damageCache,
std::shared_ptr<CBattleInfoCallback> state)
{ {
const float HEALTH_BOUNTY = 0.5; 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 // FIXME: provide distance info for Jousting bonus
auto attackerUnitForMeasurement = attacker; 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()) if(ourUnits.empty())
@ -65,15 +149,28 @@ int64_t AttackPossibility::calculateDamageReduce(
attackerUnitForMeasurement = ourUnits.front(); attackerUnitForMeasurement = ourUnits.front();
} }
auto enemyDamageBeforeAttack = cb.battleEstimateDamage(defender, attackerUnitForMeasurement, 0); auto maxHealth = defender->getMaxHealth();
auto enemiesKilled = damageDealt / defender->getMaxHealth() + (damageDealt % defender->getMaxHealth() >= defender->getFirstHPleft() ? 1 : 0); auto availableHealth = defender->getFirstHPleft() + ((defender->getCount() - 1) * maxHealth);
auto enemyDamage = averageDmg(enemyDamageBeforeAttack.damage);
auto damagePerEnemy = enemyDamage / (double)defender->getCount();
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; int64_t res = 0;
@ -84,10 +181,10 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(const BattleAttackInfo & a
auto hexes = attacker->getSurroundingHexes(hex); auto hexes = attacker->getSurroundingHexes(hex);
for(BattleHex tile : hexes) for(BattleHex tile : hexes)
{ {
auto st = state.battleGetUnitByPos(tile, true); auto st = state->battleGetUnitByPos(tile, true);
if(!st || !state.battleMatchOwner(st, attacker)) if(!st || !state->battleMatchOwner(st, attacker))
continue; continue;
if(!state.battleCanShoot(st)) if(!state->battleCanShoot(st))
continue; continue;
// FIXME: provide distance info for Jousting bonus // FIXME: provide distance info for Jousting bonus
@ -97,8 +194,8 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(const BattleAttackInfo & a
BattleAttackInfo meleeAttackInfo(st, attacker, 0, false); BattleAttackInfo meleeAttackInfo(st, attacker, 0, false);
meleeAttackInfo.defenderPos = hex; meleeAttackInfo.defenderPos = hex;
auto rangeDmg = state.battleEstimateDamage(rangeAttackInfo); auto rangeDmg = state->battleEstimateDamage(rangeAttackInfo);
auto meleeDmg = state.battleEstimateDamage(meleeAttackInfo); auto meleeDmg = state->battleEstimateDamage(meleeAttackInfo);
int64_t gain = averageDmg(rangeDmg.damage) - averageDmg(meleeDmg.damage) + 1; int64_t gain = averageDmg(rangeDmg.damage) - averageDmg(meleeDmg.damage) + 1;
res += gain; res += gain;
@ -107,13 +204,17 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(const BattleAttackInfo & a
return res; 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 attacker = attackInfo.attacker;
auto defender = attackInfo.defender; auto defender = attackInfo.defender;
const std::string cachingStringBlocksRetaliation = "type_BLOCKS_RETALIATION"; const std::string cachingStringBlocksRetaliation = "type_BLOCKS_RETALIATION";
static const auto selectorBlocksRetaliation = Selector::type()(BonusType::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); const bool counterAttacksBlocked = attacker->hasBonus(selectorBlocksRetaliation, cachingStringBlocksRetaliation);
AttackPossibility bestAp(hex, BattleHex::INVALID, attackInfo); AttackPossibility bestAp(hex, BattleHex::INVALID, attackInfo);
@ -141,9 +242,9 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf
std::vector<const battle::Unit*> units; std::vector<const battle::Unit*> units;
if (attackInfo.shooting) if (attackInfo.shooting)
units = state.getAttackedBattleUnits(attacker, defHex, true, BattleHex::INVALID); units = state->getAttackedBattleUnits(attacker, defHex, true, BattleHex::INVALID);
else else
units = state.getAttackedBattleUnits(attacker, defHex, false, hex); units = state->getAttackedBattleUnits(attacker, defHex, false, hex);
// ensure the defender is also affected // ensure the defender is also affected
bool addDefender = true; bool addDefender = true;
@ -169,10 +270,11 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf
for(int i = 0; i < totalAttacks; i++) for(int i = 0; i < totalAttacks; i++)
{ {
int64_t damageDealt, damageReceived, defenderDamageReduce, attackerDamageReduce; int64_t damageDealt, damageReceived;
float defenderDamageReduce, attackerDamageReduce;
DamageEstimation retaliation; 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.min, defenderState->getAvailableHealth());
vstd::amin(attackDmg.damage.max, 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()); vstd::amin(retaliation.damage.max, ap.attackerState->getAvailableHealth());
damageDealt = averageDmg(attackDmg.damage); damageDealt = averageDmg(attackDmg.damage);
defenderDamageReduce = calculateDamageReduce(attacker, defender, damageDealt, state); defenderDamageReduce = calculateDamageReduce(attacker, defender, damageDealt, damageCache, state);
ap.attackerState->afterAttack(attackInfo.shooting, false); ap.attackerState->afterAttack(attackInfo.shooting, false);
//FIXME: use ranged retaliation //FIXME: use ranged retaliation
@ -191,11 +293,11 @@ AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo & attackInf
if (!attackInfo.shooting && defenderState->ableToRetaliate() && !counterAttacksBlocked) if (!attackInfo.shooting && defenderState->ableToRetaliate() && !counterAttacksBlocked)
{ {
damageReceived = averageDmg(retaliation.damage); damageReceived = averageDmg(retaliation.damage);
attackerDamageReduce = calculateDamageReduce(defender, attacker, damageReceived, state); attackerDamageReduce = calculateDamageReduce(defender, attacker, damageReceived, damageCache, state);
defenderState->afterAttack(attackInfo.shooting, true); 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 // this includes enemy units as well as attacker units under enemy's mind control
if(isEnemy) 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 // 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 #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", 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 #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> /// <summary>
/// Evaluate attack value of one particular attack taking into account various effects like /// Evaluate attack value of one particular attack taking into account various effects like
/// retaliation, 2-hex breath, collateral damage, shooters blocked damage /// retaliation, 2-hex breath, collateral damage, shooters blocked damage
@ -30,24 +46,34 @@ public:
std::vector<std::shared_ptr<battle::CUnitState>> affectedUnits; std::vector<std::shared_ptr<battle::CUnitState>> affectedUnits;
int64_t defenderDamageReduce = 0; float defenderDamageReduce = 0;
int64_t attackerDamageReduce = 0; //usually by counter-attack float attackerDamageReduce = 0; //usually by counter-attack
int64_t collateralDamageReduce = 0; // friendly fire (usually by two-hex attacks) float collateralDamageReduce = 0; // friendly fire (usually by two-hex attacks)
int64_t shootersBlockedDmg = 0; int64_t shootersBlockedDmg = 0;
AttackPossibility(BattleHex from, BattleHex dest, const BattleAttackInfo & attack_); AttackPossibility(BattleHex from, BattleHex dest, const BattleAttackInfo & attack_);
int64_t damageDiff() const; float damageDiff() const;
int64_t attackValue() 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 * attacker,
const battle::Unit * defender, const battle::Unit * defender,
uint64_t damageDealt, uint64_t damageDealt,
const CBattleInfoCallback & cb); DamageCache & damageCache,
std::shared_ptr<CBattleInfoCallback> cb);
private: 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);
}; };

View File

@ -9,15 +9,18 @@
*/ */
#include "StdInc.h" #include "StdInc.h"
#include "BattleAI.h" #include "BattleAI.h"
#include "BattleEvaluator.h"
#include "BattleExchangeVariant.h" #include "BattleExchangeVariant.h"
#include "StackWithBonuses.h" #include "StackWithBonuses.h"
#include "EnemyInfo.h" #include "EnemyInfo.h"
#include "tbb/parallel_for.h"
#include "../../lib/CStopWatch.h" #include "../../lib/CStopWatch.h"
#include "../../lib/CThreadHelper.h" #include "../../lib/CThreadHelper.h"
#include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/mapObjects/CGTownInstance.h"
#include "../../lib/spells/CSpellHandler.h" #include "../../lib/spells/CSpellHandler.h"
#include "../../lib/spells/ISpellMechanics.h" #include "../../lib/spells/ISpellMechanics.h"
#include "../../lib/battle/BattleAction.h"
#include "../../lib/battle/BattleStateInfoForRetreat.h" #include "../../lib/battle/BattleStateInfoForRetreat.h"
#include "../../lib/battle/CObstacleInstance.h" #include "../../lib/battle/CObstacleInstance.h"
#include "../../lib/CStack.h" // TODO: remove #include "../../lib/CStack.h" // TODO: remove
@ -27,42 +30,6 @@
#define LOGL(text) print(text) #define LOGL(text) print(text)
#define LOGFL(text, formattingEl) print(boost::str(boost::format(text) % formattingEl)) #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> CBattleAI::getBrokenWallMoatHexes() const
{
std::vector<BattleHex> result;
for(EWallPart wallPart : { EWallPart::BOTTOM_WALL, EWallPart::BELOW_GATE, EWallPart::OVER_GATE, EWallPart::UPPER_WALL })
{
auto state = cb->battleGetWallState(wallPart);
if(state != EWallState::DESTROYED)
continue;
auto wallHex = cb->wallPartToBattleHex((EWallPart)wallPart);
auto moatHex = wallHex.cloneInDirection(BattleHex::LEFT);
result.push_back(moatHex);
}
return result;
}
CBattleAI::CBattleAI() CBattleAI::CBattleAI()
: side(-1), : side(-1),
wasWaitingForRealize(false), wasWaitingForRealize(false),
@ -85,7 +52,7 @@ void CBattleAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::share
setCbc(CB); setCbc(CB);
env = ENV; env = ENV;
cb = CB; cb = CB;
playerID = *CB->getPlayerID(); //TODO should be sth in callback playerID = *CB->getPlayerID();
wasWaitingForRealize = CB->waitTillRealize; wasWaitingForRealize = CB->waitTillRealize;
wasUnlockingGs = CB->unlockGsWhenWaiting; wasUnlockingGs = CB->unlockGsWhenWaiting;
CB->waitTillRealize = false; CB->waitTillRealize = false;
@ -93,9 +60,15 @@ void CBattleAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::share
movesSkippedByDefense = 0; movesSkippedByDefense = 0;
} }
BattleAction CBattleAI::useHealingTent(const CStack *stack) void CBattleAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB, AutocombatPreferences autocombatPreferences)
{ {
auto healingTargets = cb->battleGetStacks(CBattleInfoEssentials::ONLY_MINE); initBattleInterface(ENV, CB);
autobattlePreferences = autocombatPreferences;
}
BattleAction CBattleAI::useHealingTent(const BattleID & battleID, const CStack *stack)
{
auto healingTargets = cb->getBattle(battleID)->battleGetStacks(CBattleInfoEssentials::ONLY_MINE);
std::map<int, const CStack*> woundHpToStack; std::map<int, const CStack*> woundHpToStack;
for(const auto * stack : healingTargets) for(const auto * stack : healingTargets)
{ {
@ -109,161 +82,42 @@ BattleAction CBattleAI::useHealingTent(const CStack *stack)
return BattleAction::makeHeal(stack, woundHpToStack.rbegin()->second); //last element of the woundHpToStack is the most wounded stack return BattleAction::makeHeal(stack, woundHpToStack.rbegin()->second); //last element of the woundHpToStack is the most wounded stack
} }
std::optional<PossibleSpellcast> CBattleAI::findBestCreatureSpell(const CStack *stack) void CBattleAI::yourTacticPhase(const BattleID & battleID, int distance)
{ {
//TODO: faerie dragon type spell should be selected by server cb->battleMakeTacticAction(battleID, BattleAction::makeEndOFTacticPhase(cb->getBattle(battleID)->battleGetTacticsSide()));
SpellID creatureSpellToCast = cb->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(getCbc().get(), spells::Mode::CREATURE_ACTIVE, stack))
{
std::vector<PossibleSpellcast> possibleCasts;
spells::BattleCast temp(getCbc().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 CBattleAI::selectStackAction(const CStack * stack) static float getStrengthRatio(std::shared_ptr<CBattleInfoCallback> cb, int side)
{ {
//evaluate casting spell for spellcasting stack auto stacks = cb->battleGetAllStacks();
std::optional<PossibleSpellcast> bestSpellcast = findBestCreatureSpell(stack); auto our = 0, enemy = 0;
HypotheticBattle hb(env.get(), cb); for(auto stack : stacks)
PotentialTargets targets(stack, hb);
BattleExchangeEvaluator scoreEvaluator(cb, env);
auto moveTarget = scoreEvaluator.findMoveTowardsUnreachable(stack, targets, hb);
int64_t score = EvaluationResult::INEFFECTIVE_SCORE;
if(targets.possibleAttacks.empty() && bestSpellcast.has_value())
{ {
movesSkippedByDefense = 0; auto creature = stack->creatureId().toCreature();
return BattleAction::makeCreatureSpellcast(stack, bestSpellcast->dest, bestSpellcast->spell->id);
}
if(!targets.possibleAttacks.empty()) if(!creature)
{ continue;
#if BATTLE_TRACE_LEVEL>=1
logAi->trace("Evaluating attack for %s", stack->getDescription());
#endif
auto evaluationResult = scoreEvaluator.findBestTarget(stack, targets, hb); if(stack->unitSide() == side)
auto & bestAttack = evaluationResult.bestAttack; our += stack->getCount() * creature->getAIValue();
//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
movesSkippedByDefense = 0;
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: +%lld -%lld = %lld",
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.score <= score)
{
if(evaluationResult.wait)
{
return BattleAction::makeWait(stack);
}
else if(bestAttack.attack.shooting)
{
movesSkippedByDefense = 0;
return BattleAction::makeShotAttack(stack, bestAttack.attack.defender);
}
else else
{ enemy += stack->getCount() * creature->getAIValue();
movesSkippedByDefense = 0;
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. return enemy == 0 ? 1.0f : static_cast<float>(our) / enemy;
if(moveTarget.score > score)
{
score = moveTarget.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->battleGetSiegeLevel() >= CGTownInstance::CITADEL)
{
auto brokenWallMoat = getBrokenWallMoatHexes();
if(brokenWallMoat.size())
{
movesSkippedByDefense = 0;
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);
} }
void CBattleAI::yourTacticPhase(int distance) void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack )
{ {
cb->battleMakeTacticAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide())); LOG_TRACE_PARAMS(logAi, "stack: %s", stack->nodeName());
}
uint64_t timeElapsed(std::chrono::time_point<std::chrono::high_resolution_clock> start) auto timeElapsed = [](std::chrono::time_point<std::chrono::high_resolution_clock> start) -> uint64_t
{ {
auto end = std::chrono::high_resolution_clock::now(); auto end = std::chrono::high_resolution_clock::now();
return std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count(); return std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
} };
void CBattleAI::activeStack( const CStack * stack )
{
LOG_TRACE_PARAMS(logAi, "stack: %s", stack->nodeName());
BattleAction result = BattleAction::makeDefend(stack); BattleAction result = BattleAction::makeDefend(stack);
setCbc(cb); //TODO: make solid sure that AIs always use their callbacks (need to take care of event handlers too) setCbc(cb); //TODO: make solid sure that AIs always use their callbacks (need to take care of event handlers too)
@ -274,36 +128,40 @@ void CBattleAI::activeStack( const CStack * stack )
{ {
if(stack->creatureId() == CreatureID::CATAPULT) if(stack->creatureId() == CreatureID::CATAPULT)
{ {
cb->battleMakeUnitAction(useCatapult(stack)); cb->battleMakeUnitAction(battleID, useCatapult(battleID, stack));
return; return;
} }
if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON) && stack->hasBonusOfType(BonusType::HEALER)) if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON) && stack->hasBonusOfType(BonusType::HEALER))
{ {
cb->battleMakeUnitAction(useHealingTent(stack)); cb->battleMakeUnitAction(battleID, useHealingTent(battleID, stack));
return; return;
} }
attemptCastingSpell(); #if BATTLE_TRACE_LEVEL>=1
logAi->trace("Build evaluator and targets");
#endif
BattleEvaluator evaluator(env, cb, stack, playerID, battleID, side, getStrengthRatio(cb->getBattle(battleID), side));
result = evaluator.selectStackAction(stack);
if(!skipCastUntilNextBattle && evaluator.canCastSpell())
{
auto spelCasted = evaluator.attemptCastingSpell(stack);
if(spelCasted)
return;
skipCastUntilNextBattle = true;
}
logAi->trace("Spellcast attempt completed in %lld", timeElapsed(start)); logAi->trace("Spellcast attempt completed in %lld", timeElapsed(start));
if(cb->battleIsFinished() || !stack->alive()) if(auto action = considerFleeingOrSurrendering(battleID))
{ {
//spellcast may finish battle or kill active stack cb->battleMakeUnitAction(battleID, *action);
//send special preudo-action
BattleAction cancel;
cancel.actionType = EActionType::CANCEL;
cb->battleMakeUnitAction(cancel);
return; return;
} }
if(auto action = considerFleeingOrSurrendering())
{
cb->battleMakeUnitAction(*action);
return;
}
result = selectStackAction(stack);
} }
catch(boost::thread_interrupted &) catch(boost::thread_interrupted &)
{ {
@ -325,117 +183,17 @@ void CBattleAI::activeStack( const CStack * stack )
logAi->trace("BattleAI decission made in %lld", timeElapsed(start)); logAi->trace("BattleAI decission made in %lld", timeElapsed(start));
cb->battleMakeUnitAction(result); cb->battleMakeUnitAction(battleID, result);
} }
BattleAction CBattleAI::goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes) const BattleAction CBattleAI::useCatapult(const BattleID & battleID, const CStack * stack)
{
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);
}
BattleExchangeEvaluator scoreEvaluator(cb, env);
HypotheticBattle hb(env.get(), cb);
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];
}
}
}
BattleAction CBattleAI::useCatapult(const CStack * stack)
{ {
BattleAction attack; BattleAction attack;
BattleHex targetHex = BattleHex::INVALID; BattleHex targetHex = BattleHex::INVALID;
if(cb->battleGetGateState() == EGateState::CLOSED) if(cb->getBattle(battleID)->battleGetGateState() == EGateState::CLOSED)
{ {
targetHex = cb->wallPartToBattleHex(EWallPart::GATE); targetHex = cb->getBattle(battleID)->wallPartToBattleHex(EWallPart::GATE);
} }
else else
{ {
@ -451,11 +209,11 @@ BattleAction CBattleAI::useCatapult(const CStack * stack)
for(auto wallPart : wallParts) for(auto wallPart : wallParts)
{ {
auto wallState = cb->battleGetWallState(wallPart); auto wallState = cb->getBattle(battleID)->battleGetWallState(wallPart);
if(wallState == EWallState::REINFORCED || wallState == EWallState::INTACT || wallState == EWallState::DAMAGED) if(wallState == EWallState::REINFORCED || wallState == EWallState::INTACT || wallState == EWallState::DAMAGED)
{ {
targetHex = cb->wallPartToBattleHex(wallPart); targetHex = cb->getBattle(battleID)->wallPartToBattleHex(wallPart);
break; break;
} }
} }
@ -476,396 +234,30 @@ BattleAction CBattleAI::useCatapult(const CStack * stack)
return attack; return attack;
} }
void CBattleAI::attemptCastingSpell() void CBattleAI::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed)
{
auto hero = cb->battleGetMyHero();
if(!hero)
return;
if(cb->battleCanCastSpell(hero, spells::Mode::HERO) != ESpellCastProblem::OK)
return;
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.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;
});
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.get(), hero, spells::Mode::HERO, spell);
if(!spell->isDamage() && 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;
using ValueMap = PossibleSpellcast::ValueMap;
auto evaluateQueue = [&](ValueMap & values, const std::vector<battle::Units> & queue, 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(0);//todo: set actual value?
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, 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->battleGetAllStacks(false);
size_t ourRemainingTurns = 0;
for(auto unit : all)
{
healthOfStack[unit->unitId()] = unit->getAvailableHealth();
valueOfStack[unit->unitId()] = 0;
if(cb->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->battleGetTurnOrder(turnOrder, amount, 2); //no more than 1 turn after current, each unit at least once
{
bool enemyHadTurn = false;
HypotheticBattle state(env.get(), cb);
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;
}
}
}
struct ScriptsCache
{
//todo: re-implement scripts context cache
};
auto evaluateSpellcast = [&] (PossibleSpellcast * ps, std::shared_ptr<ScriptsCache>)
{
HypotheticBattle state(env.get(), cb);
spells::BattleCast cast(&state, hero, spells::Mode::HERO, ps->spell);
cast.castEval(state.getServerCallback(), ps->dest);
ValueMap newHealthOfStack;
ValueMap newValueOfStack;
size_t ourUnits = 0;
std::set<uint32_t> unitIds;
state.battleGetUnitsIf([&](const battle::Unit * u)->bool
{
if(!u->isGhost() && !u->isTurret())
unitIds.insert(u->unitId());
return false;
});
for(auto unitId : unitIds)
{
auto localUnit = state.battleGetUnitByID(unitId);
newHealthOfStack[unitId] = localUnit->getAvailableHealth();
newValueOfStack[unitId] = 0;
if(state.battleGetOwner(localUnit) == playerID && localUnit->alive() && localUnit->willMove())
ourUnits++;
}
size_t minTurnSpan = ourUnits/3; //todo: tweak this
std::vector<battle::Units> newTurnOrder;
state.battleGetTurnOrder(newTurnOrder, amount, 2);
const bool turnSpanOK = evaluateQueue(newValueOfStack, newTurnOrder, state, minTurnSpan, nullptr);
if(turnSpanOK || castNow)
{
int64_t totalGain = 0;
for(auto unitId : unitIds)
{
auto localUnit = state.battleGetUnitByID(unitId);
auto newValue = getValOr(newValueOfStack, unitId, 0);
auto oldValue = getValOr(valueOfStack, unitId, 0);
auto healthDiff = newHealthOfStack[unitId] - healthOfStack[unitId];
if(localUnit->unitOwner() != playerID)
healthDiff = -healthDiff;
if(healthDiff < 0)
{
ps->value = -1;
return; //do not damage own units at all
}
totalGain += (newValue - oldValue + healthDiff);
}
ps->value = totalGain;
}
else
{
ps->value = -1;
}
};
using EvalRunner = ThreadPool<ScriptsCache>;
EvalRunner::Tasks tasks;
for(PossibleSpellcast & psc : possibleCasts)
tasks.push_back(std::bind(evaluateSpellcast, &psc, _1));
uint32_t threadCount = boost::thread::hardware_concurrency();
if(threadCount == 0)
{
logGlobal->warn("No information of CPU cores available");
threadCount = 1;
}
CStopWatch timer;
std::vector<std::shared_ptr<ScriptsCache>> scriptsPool;
for(uint32_t idx = 0; idx < threadCount; idx++)
{
scriptsPool.emplace_back();
}
EvalRunner runner(&tasks, scriptsPool);
runner.run();
LOGFL("Evaluation took %d ms", timer.getDiff());
auto pscValue = [](const PossibleSpellcast &ps) -> int64_t
{
return ps.value;
};
auto castToPerform = *vstd::maxElementByFun(possibleCasts, pscValue);
if(castToPerform.value > 0)
{
LOGFL("Best spell is %s (value %d). Will cast.", castToPerform.spell->getNameTranslated() % castToPerform.value);
BattleAction spellcast;
spellcast.actionType = EActionType::HERO_SPELL;
spellcast.actionSubtype = castToPerform.spell->id;
spellcast.setTarget(castToPerform.dest);
spellcast.side = side;
spellcast.stackNumber = (!side) ? -1 : -2;
cb->battleMakeSpellAction(spellcast);
movesSkippedByDefense = 0;
}
else
{
LOGFL("Best spell is %s. But it is actually useless (value %d).", castToPerform.spell->getNameTranslated() % castToPerform.value);
}
}
//Below method works only for offensive spells
void CBattleAI::evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps)
{
using ValueMap = PossibleSpellcast::ValueMap;
RNGStub rngStub;
HypotheticBattle state(env.get(), cb);
TStacks all = cb->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() != getCbc()->getPlayerID())
healthDiff = -healthDiff;
if(healthDiff < 0)
{
ps.value = -1;
return; //do not damage own units at all
}
totalGain += healthDiff;
}
ps.value = totalGain;
}
void CBattleAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed)
{ {
LOG_TRACE(logAi); LOG_TRACE(logAi);
side = Side; side = Side;
skipCastUntilNextBattle = false;
} }
void CBattleAI::print(const std::string &text) const void CBattleAI::print(const std::string &text) const
{ {
logAi->trace("%s Battle AI[%p]: %s", playerID.getStr(), this, text); logAi->trace("%s Battle AI[%p]: %s", playerID.toString(), this, text);
} }
std::optional<BattleAction> CBattleAI::considerFleeingOrSurrendering() std::optional<BattleAction> CBattleAI::considerFleeingOrSurrendering(const BattleID & battleID)
{ {
BattleStateInfoForRetreat bs; BattleStateInfoForRetreat bs;
bs.canFlee = cb->battleCanFlee(); bs.canFlee = cb->getBattle(battleID)->battleCanFlee();
bs.canSurrender = cb->battleCanSurrender(playerID); bs.canSurrender = cb->getBattle(battleID)->battleCanSurrender(playerID);
bs.ourSide = cb->battleGetMySide(); bs.ourSide = cb->getBattle(battleID)->battleGetMySide();
bs.ourHero = cb->battleGetMyHero(); bs.ourHero = cb->getBattle(battleID)->battleGetMyHero();
bs.enemyHero = nullptr; bs.enemyHero = nullptr;
for(auto stack : cb->battleGetAllStacks(false)) for(auto stack : cb->getBattle(battleID)->battleGetAllStacks(false))
{ {
if(stack->alive()) if(stack->alive())
{ {
@ -874,7 +266,7 @@ std::optional<BattleAction> CBattleAI::considerFleeingOrSurrendering()
else else
{ {
bs.enemyStacks.push_back(stack); bs.enemyStacks.push_back(stack);
bs.enemyHero = cb->battleGetOwnerHero(stack); bs.enemyHero = cb->getBattle(battleID)->battleGetOwnerHero(stack);
} }
} }
} }
@ -886,7 +278,7 @@ std::optional<BattleAction> CBattleAI::considerFleeingOrSurrendering()
return std::nullopt; return std::nullopt;
} }
auto result = cb->makeSurrenderRetreatDecision(bs); auto result = cb->makeSurrenderRetreatDecision(battleID, bs);
if(!result && bs.canFlee && bs.turnsSkippedByDefense > 30) if(!result && bs.canFlee && bs.turnsSkippedByDefense > 30)
{ {

View File

@ -62,28 +62,25 @@ class CBattleAI : public CBattleGameInterface
bool wasWaitingForRealize; bool wasWaitingForRealize;
bool wasUnlockingGs; bool wasUnlockingGs;
int movesSkippedByDefense; int movesSkippedByDefense;
bool skipCastUntilNextBattle;
public: public:
CBattleAI(); CBattleAI();
~CBattleAI(); ~CBattleAI();
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) 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 std::optional<BattleAction> considerFleeingOrSurrendering(const BattleID & battleID);
void yourTacticPhase(int distance) override;
std::optional<BattleAction> considerFleeingOrSurrendering();
void print(const std::string &text) const; void print(const std::string &text) const;
BattleAction useCatapult(const CStack *stack); BattleAction useCatapult(const BattleID & battleID, const CStack *stack);
BattleAction useHealingTent(const CStack *stack); BattleAction useHealingTent(const BattleID & battleID, const CStack *stack);
BattleAction selectStackAction(const CStack * stack);
std::optional<PossibleSpellcast> findBestCreatureSpell(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 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 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 //void battleAttack(const BattleAttack *ba) override; //called when stack is performing attack
@ -98,8 +95,5 @@ public:
//void battleTriggerEffect(const BattleTriggerEffect & bte) override; //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 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 //void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack
AutocombatPreferences autobattlePreferences = AutocombatPreferences();
private:
BattleAction goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes) const;
std::vector<BattleHex> getBrokenWallMoatHexes() const;
}; };

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() MoveTarget::MoveTarget()
: positions() : positions(), cachedAttack()
{ {
score = EvaluationResult::INEFFECTIVE_SCORE; 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; auto affectedUnits = ap.affectedUnits;
affectedUnits.push_back(ap.attackerState); affectedUnits.push_back(ap.attackerState);
for(auto affectedUnit : affectedUnits) for(auto affectedUnit : affectedUnits)
{ {
auto unitToUpdate = state.getForUpdate(affectedUnit->unitId()); auto unitToUpdate = hb->getForUpdate(affectedUnit->unitId());
unitToUpdate->health = affectedUnit->health; if(unitToUpdate->unitSide() == attacker->unitSide())
unitToUpdate->shots = affectedUnit->shots; {
unitToUpdate->counterAttacks = affectedUnit->counterAttacks; if(unitToUpdate->unitId() == attacker->unitId())
unitToUpdate->movedThisRound = affectedUnit->movedThisRound; {
} 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 #if BATTLE_TRACE_LEVEL>=1
logAi->trace( logAi->trace(
"%s -> %s, ap attack, %s, dps: %lld, score: %lld", "%s -> %s, ap retalitation, %s, dps: %2f, score: %2f",
ap.attack.attacker->getDescription(), defender->getDescription(),
ap.attack.defender->getDescription(), unitToUpdate->getDescription(),
ap.attack.shooting ? "shot" : "mellee", ap.attack.shooting ? "shot" : "mellee",
ap.damageDealt, retaliationDamage,
attackValue); attackerDamageReduce);
#endif #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; return attackValue;
} }
int64_t BattleExchangeVariant::trackAttack( float BattleExchangeVariant::trackAttack(
std::shared_ptr<StackWithBonuses> attacker, std::shared_ptr<StackWithBonuses> attacker,
std::shared_ptr<StackWithBonuses> defender, std::shared_ptr<StackWithBonuses> defender,
bool shooting, bool shooting,
bool isOurAttack, bool isOurAttack,
const CBattleInfoCallback & cb, DamageCache & damageCache,
std::shared_ptr<HypotheticBattle> hb,
bool evaluateOnly) bool evaluateOnly)
{ {
const std::string cachingStringBlocksRetaliation = "type_BLOCKS_RETALIATION"; const std::string cachingStringBlocksRetaliation = "type_BLOCKS_RETALIATION";
static const auto selectorBlocksRetaliation = Selector::type()(BonusType::BLOCKS_RETALIATION); static const auto selectorBlocksRetaliation = Selector::type()(BonusType::BLOCKS_RETALIATION);
const bool counterAttacksBlocked = attacker->hasBonus(selectorBlocksRetaliation, cachingStringBlocksRetaliation); const bool counterAttacksBlocked = attacker->hasBonus(selectorBlocksRetaliation, cachingStringBlocksRetaliation);
DamageEstimation retaliation; int64_t attackDamage = damageCache.getDamage(attacker.get(), defender.get(), hb);
// FIXME: provide distance info for Jousting bonus float defenderDamageReduce = AttackPossibility::calculateDamageReduce(attacker.get(), defender.get(), attackDamage, damageCache, hb);
BattleAttackInfo bai(attacker.get(), defender.get(), 0, shooting); float attackerDamageReduce = 0;
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;
if(!evaluateOnly) if(!evaluateOnly)
{ {
#if BATTLE_TRACE_LEVEL>=1 #if BATTLE_TRACE_LEVEL>=1
logAi->trace( logAi->trace(
"%s -> %s, normal attack, %s, dps: %lld, %lld", "%s -> %s, normal attack, %s, dps: %lld, %2f",
attacker->getDescription(), attacker->getDescription(),
defender->getDescription(), defender->getDescription(),
shooting ? "shot" : "mellee", shooting ? "shot" : "mellee",
@ -96,28 +156,24 @@ int64_t BattleExchangeVariant::trackAttack(
if(isOurAttack) if(isOurAttack)
{ {
dpsScore += defenderDamageReduce; dpsScore += defenderDamageReduce * positiveEffectMultiplier;
attackerValue[attacker->unitId()].value += defenderDamageReduce; attackerValue[attacker->unitId()].value += defenderDamageReduce;
} }
else else
dpsScore -= defenderDamageReduce; dpsScore -= defenderDamageReduce * negativeEffectMultiplier;
defender->damage(attackDamage); defender->damage(attackDamage);
attacker->afterAttack(shooting, false); 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 = damageCache.getDamage(defender.get(), attacker.get(), hb);
{ attackerDamageReduce = AttackPossibility::calculateDamageReduce(defender.get(), attacker.get(), retaliationDamage, damageCache, hb);
auto retaliationDamage = (retaliation.damage.min + retaliation.damage.max) / 2;
attackerDamageReduce = AttackPossibility::calculateDamageReduce(defender.get(), attacker.get(), retaliationDamage, cb);
if(!evaluateOnly)
{
#if BATTLE_TRACE_LEVEL>=1 #if BATTLE_TRACE_LEVEL>=1
logAi->trace( logAi->trace(
"%s -> %s, retaliation, dps: %lld, %lld", "%s -> %s, retaliation, dps: %lld, %2f",
defender->getDescription(), defender->getDescription(),
attacker->getDescription(), attacker->getDescription(),
retaliationDamage, retaliationDamage,
@ -126,64 +182,55 @@ int64_t BattleExchangeVariant::trackAttack(
if(isOurAttack) if(isOurAttack)
{ {
dpsScore -= attackerDamageReduce; dpsScore -= attackerDamageReduce * negativeEffectMultiplier;
attackerValue[attacker->unitId()].isRetalitated = true; attackerValue[attacker->unitId()].isRetalitated = true;
} }
else else
{ {
dpsScore += attackerDamageReduce; dpsScore += attackerDamageReduce * positiveEffectMultiplier;
attackerValue[defender->unitId()].value += attackerDamageReduce; attackerValue[defender->unitId()].value += attackerDamageReduce;
} }
attacker->damage(retaliationDamage); attacker->damage(retaliationDamage);
defender->afterAttack(false, true); defender->afterAttack(false, true);
} }
}
}
auto score = defenderDamageReduce - attackerDamageReduce; auto score = defenderDamageReduce - attackerDamageReduce;
#if BATTLE_TRACE_LEVEL>=1 #if BATTLE_TRACE_LEVEL>=1
if(!score) 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 #endif
return score; 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()); 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(!activeStack->waited())
{ {
#if BATTLE_TRACE_LEVEL>=1 #if BATTLE_TRACE_LEVEL>=1
logAi->trace("Evaluating waited attack for %s", activeStack->getDescription()); logAi->trace("Evaluating waited attack for %s", activeStack->getDescription());
#endif #endif
hb.getForUpdate(activeStack->unitId())->waiting = true; auto hbWaited = std::make_shared<HypotheticBattle>(env.get(), hb);
hb.getForUpdate(activeStack->unitId())->waitedThisTurn = true;
updateReachabilityMap(hb); hbWaited->getForUpdate(activeStack->unitId())->waiting = true;
hbWaited->getForUpdate(activeStack->unitId())->waitedThisTurn = true;
updateReachabilityMap(hbWaited);
for(auto & ap : targets.possibleAttacks) for(auto & ap : targets.possibleAttacks)
{ {
int64_t score = calculateExchange(ap, targets, hb); float score = calculateExchange(ap, targets, damageCache, hbWaited);
if(score > result.score) 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; 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; MoveTarget result;
BattleExchangeVariant ev; BattleExchangeVariant ev(getPositiveEffectMultiplier(), getNegativeEffectMultiplier());
if(targets.unreachableEnemies.empty()) if(targets.unreachableEnemies.empty())
return result; return result;
@ -237,16 +306,20 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(const battle::Uni
{ {
// FIXME: provide distance info for Jousting bonus // FIXME: provide distance info for Jousting bonus
auto bai = BattleAttackInfo(activeStack, closestStack, 0, cb->battleCanShoot(activeStack)); 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 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.score = score;
result.positions = closestStack->getAttackableHexes(activeStack); 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( std::vector<const battle::Unit *> BattleExchangeEvaluator::getExchangeUnits(
const AttackPossibility & ap, const AttackPossibility & ap,
PotentialTargets & targets, PotentialTargets & targets,
HypotheticBattle & hb) std::shared_ptr<HypotheticBattle> hb)
{ {
auto hexes = ap.attack.defender->getHexes(); auto hexes = ap.attack.defender->getHexes();
@ -308,7 +381,7 @@ std::vector<const battle::Unit *> BattleExchangeEvaluator::getExchangeUnits(
{ {
for(auto adjacentUnit : getAdjacentUnits(unit)) for(auto adjacentUnit : getAdjacentUnits(unit))
{ {
auto unitWithBonuses = hb.battleGetUnitByID(adjacentUnit->unitId()); auto unitWithBonuses = hb->battleGetUnitByID(adjacentUnit->unitId());
if(vstd::contains(targets.unreachableEnemies, adjacentUnit) if(vstd::contains(targets.unreachableEnemies, adjacentUnit)
&& !vstd::contains(allReachableUnits, unitWithBonuses)) && !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; return exchangeUnits;
} }
int64_t BattleExchangeEvaluator::calculateExchange( float BattleExchangeEvaluator::calculateExchange(
const AttackPossibility & ap, const AttackPossibility & ap,
PotentialTargets & targets, PotentialTargets & targets,
HypotheticBattle & hb) DamageCache & damageCache,
std::shared_ptr<HypotheticBattle> hb)
{ {
#if BATTLE_TRACE_LEVEL>=1 #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 #endif
if(cb->battleGetMySide() == BattlePerspective::LEFT_SIDE if(cb->battleGetMySide() == BattlePerspective::LEFT_SIDE
&& cb->battleGetGateState() == EGateState::BLOCKED && cb->battleGetGateState() == EGateState::BLOCKED
&& ap.attack.defender->coversPos(ESiegeHex::GATE_BRIDGE)) && ap.attack.defender->coversPos(BattleHex::GATE_BRIDGE))
{ {
return EvaluationResult::INEFFECTIVE_SCORE; return EvaluationResult::INEFFECTIVE_SCORE;
} }
@ -365,6 +444,7 @@ int64_t BattleExchangeEvaluator::calculateExchange(
std::vector<const battle::Unit *> ourStacks; std::vector<const battle::Unit *> ourStacks;
std::vector<const battle::Unit *> enemyStacks; std::vector<const battle::Unit *> enemyStacks;
if(hb->battleGetUnitByID(ap.attack.defender->unitId())->alive())
enemyStacks.push_back(ap.attack.defender); enemyStacks.push_back(ap.attack.defender);
std::vector<const battle::Unit *> exchangeUnits = getExchangeUnits(ap, targets, hb); std::vector<const battle::Unit *> exchangeUnits = getExchangeUnits(ap, targets, hb);
@ -374,8 +454,23 @@ int64_t BattleExchangeEvaluator::calculateExchange(
return 0; return 0;
} }
HypotheticBattle exchangeBattle(env.get(), cb); auto exchangeBattle = std::make_shared<HypotheticBattle>(env.get(), hb);
BattleExchangeVariant v; 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; auto melleeAttackers = ourStacks;
vstd::removeDuplicates(melleeAttackers); vstd::removeDuplicates(melleeAttackers);
@ -384,30 +479,15 @@ int64_t BattleExchangeEvaluator::calculateExchange(
return !cb->battleCanShoot(u); 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; bool canUseAp = true;
for(auto activeUnit : exchangeUnits) 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 & attackerQueue = isOur ? ourStacks : enemyStacks;
battle::Units & oppositeQueue = isOur ? enemyStacks : ourStacks; battle::Units & oppositeQueue = isOur ? enemyStacks : ourStacks;
auto attacker = exchangeBattle.getForUpdate(activeUnit->unitId()); auto attacker = exchangeBattle->getForUpdate(activeUnit->unitId());
if(!attacker->alive()) if(!attacker->alive())
{ {
@ -420,21 +500,22 @@ int64_t BattleExchangeEvaluator::calculateExchange(
auto targetUnit = ap.attack.defender; 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( auto score = v.trackAttack(
attacker, attacker,
stackWithBonuses, stackWithBonuses,
exchangeBattle.battleCanShoot(stackWithBonuses.get()), exchangeBattle->battleCanShoot(stackWithBonuses.get()),
isOur, isOur,
*cb, damageCache,
hb,
true); true);
#if BATTLE_TRACE_LEVEL>=1 #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 #endif
return score; return score;
@ -446,9 +527,12 @@ int64_t BattleExchangeEvaluator::calculateExchange(
} }
else 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 false;
return vstd::contains_if(reachabilityMap[u->getPosition()], [&](const battle::Unit * other) -> bool 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 defender = exchangeBattle->getForUpdate(targetUnit->unitId());
auto shooting = cb->battleCanShoot(attacker.get()); auto shooting = exchangeBattle->battleCanShoot(attacker.get());
const int totalAttacks = attacker->getTotalAttacks(shooting); 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 else
{ {
for(int i = 0; i < totalAttacks; i++) 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()) if(!attacker->alive() || !defender->alive())
break; break;
@ -495,12 +580,12 @@ int64_t BattleExchangeEvaluator::calculateExchange(
vstd::erase_if(attackerQueue, [&](const battle::Unit * u) -> bool 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 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); v.adjustPositions(melleeAttackers, ap, reachabilityMap);
#if BATTLE_TRACE_LEVEL>=1 #if BATTLE_TRACE_LEVEL>=1
logAi->trace("Exchange score: %lld", v.getScore()); logAi->trace("Exchange score: %2f", v.getScore());
#endif #endif
return v.getScore(); return v.getScore();
@ -539,7 +624,7 @@ void BattleExchangeVariant::adjustPositions(
vstd::erase_if_present(hexes, ap.attack.attacker->occupiedHex(ap.attack.attackerPos)); vstd::erase_if_present(hexes, ap.attack.attacker->occupiedHex(ap.attack.attackerPos));
} }
int64_t notRealizedDamage = 0; float notRealizedDamage = 0;
for(auto unit : attackers) for(auto unit : attackers)
{ {
@ -555,7 +640,7 @@ void BattleExchangeVariant::adjustPositions(
continue; 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) auto score = vstd::contains(reachabilityMap[h], unit)
? reachabilityMap[h].size() ? 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; const int TURN_DEPTH = 2;
turnOrder.clear(); turnOrder.clear();
hb.battleGetTurnOrder(turnOrder, std::numeric_limits<int>::max(), TURN_DEPTH); hb->battleGetTurnOrder(turnOrder, std::numeric_limits<int>::max(), TURN_DEPTH);
reachabilityMap.clear(); reachabilityMap.clear();
for(int turn = 0; turn < turnOrder.size(); turn++) for(int turn = 0; turn < turnOrder.size(); turn++)

View File

@ -16,7 +16,7 @@
struct AttackerValue struct AttackerValue
{ {
int64_t value; float value;
bool isRetalitated; bool isRetalitated;
BattleHex position; BattleHex position;
@ -25,20 +25,23 @@ struct AttackerValue
struct MoveTarget struct MoveTarget
{ {
int64_t score; float score;
float scorePerTurn;
std::vector<BattleHex> positions; std::vector<BattleHex> positions;
std::optional<AttackPossibility> cachedAttack;
uint8_t turnsToRich;
MoveTarget(); MoveTarget();
}; };
struct EvaluationResult struct EvaluationResult
{ {
static const int64_t INEFFECTIVE_SCORE = -1000000; static const int64_t INEFFECTIVE_SCORE = -10000;
AttackPossibility bestAttack; AttackPossibility bestAttack;
MoveTarget bestMove; MoveTarget bestMove;
bool wait; bool wait;
int64_t score; float score;
bool defend; bool defend;
EvaluationResult(const AttackPossibility & ap) EvaluationResult(const AttackPossibility & ap)
@ -56,19 +59,24 @@ struct EvaluationResult
class BattleExchangeVariant class BattleExchangeVariant
{ {
public: 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> attacker,
std::shared_ptr<StackWithBonuses> defender, std::shared_ptr<StackWithBonuses> defender,
bool shooting, bool shooting,
bool isOurAttack, bool isOurAttack,
const CBattleInfoCallback & cb, DamageCache & damageCache,
std::shared_ptr<HypotheticBattle> hb,
bool evaluateOnly = false); bool evaluateOnly = false);
int64_t getScore() const { return dpsScore; } float getScore() const { return dpsScore; }
void adjustPositions( void adjustPositions(
std::vector<const battle::Unit *> attackers, std::vector<const battle::Unit *> attackers,
@ -76,7 +84,9 @@ public:
std::map<BattleHex, battle::Units> & reachabilityMap); std::map<BattleHex, battle::Units> & reachabilityMap);
private: private:
int64_t dpsScore; float positiveEffectMultiplier;
float negativeEffectMultiplier;
float dpsScore;
std::map<uint32_t, AttackerValue> attackerValue; std::map<uint32_t, AttackerValue> attackerValue;
}; };
@ -87,15 +97,40 @@ private:
std::shared_ptr<Environment> env; std::shared_ptr<Environment> env;
std::map<BattleHex, std::vector<const battle::Unit *>> reachabilityMap; std::map<BattleHex, std::vector<const battle::Unit *>> reachabilityMap;
std::vector<battle::Units> turnOrder; std::vector<battle::Units> turnOrder;
float negativeEffectMultiplier;
public: 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); EvaluationResult findBestTarget(
int64_t calculateExchange(const AttackPossibility & ap, PotentialTargets & targets, HypotheticBattle & hb); const battle::Unit * activeStack,
void updateReachabilityMap(HypotheticBattle & hb); PotentialTargets & targets,
std::vector<const battle::Unit *> getExchangeUnits(const AttackPossibility & ap, PotentialTargets & targets, HypotheticBattle & hb); 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); 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); 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 set(battleAI_SRCS
AttackPossibility.cpp AttackPossibility.cpp
BattleAI.cpp BattleAI.cpp
BattleEvaluator.cpp
common.cpp common.cpp
EnemyInfo.cpp EnemyInfo.cpp
PossibleSpellcast.cpp PossibleSpellcast.cpp
@ -15,6 +16,7 @@ set(battleAI_HEADERS
AttackPossibility.h AttackPossibility.h
BattleAI.h BattleAI.h
BattleEvaluator.h
common.h common.h
EnemyInfo.h EnemyInfo.h
PotentialTargets.h PotentialTargets.h
@ -37,7 +39,11 @@ else()
endif() endif()
target_include_directories(BattleAI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) 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") vcmi_set_output_dir(BattleAI "AI")
enable_pch(BattleAI) 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; const CSpell * spell;
spells::Target dest; spells::Target dest;
int64_t value; float value;
PossibleSpellcast(); PossibleSpellcast();
virtual ~PossibleSpellcast(); virtual ~PossibleSpellcast();

View File

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

View File

@ -17,7 +17,10 @@ public:
std::vector<const battle::Unit *> unreachableEnemies; std::vector<const battle::Unit *> unreachableEnemies;
PotentialTargets(){}; 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; const AttackPossibility & bestAction() const;
int64_t bestActionValue() const; int64_t bestActionValue() const;

View File

@ -45,13 +45,32 @@ StackWithBonuses::StackWithBonuses(const HypotheticBattle * Owner, const battle:
id(Stack->unitId()), id(Stack->unitId()),
side(Stack->unitSide()), side(Stack->unitSide()),
player(Stack->unitOwner()), player(Stack->unitOwner()),
slot(Stack->unitSlot()) slot(Stack->unitSlot()),
treeVersionLocal(0)
{ {
localInit(Owner); localInit(Owner);
battle::CUnitState::operator=(*Stack); 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) StackWithBonuses::StackWithBonuses(const HypotheticBattle * Owner, const battle::UnitInfo & info)
: battle::CUnitState(), : battle::CUnitState(),
origBearer(nullptr), origBearer(nullptr),
@ -59,7 +78,8 @@ StackWithBonuses::StackWithBonuses(const HypotheticBattle * Owner, const battle:
baseAmount(info.count), baseAmount(info.count),
id(info.id), id(info.id),
side(info.side), side(info.side),
slot(SlotID::SUMMONED_SLOT_PLACEHOLDER) slot(SlotID::SUMMONED_SLOT_PLACEHOLDER),
treeVersionLocal(0)
{ {
type = info.type.toCreature(); type = info.type.toCreature();
origBearer = type; origBearer = type;
@ -124,7 +144,7 @@ TConstBonusListPtr StackWithBonuses::getAllBonuses(const CSelector & selector, c
for(const Bonus & bonus : bonusesToUpdate) 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)))) 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 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) void StackWithBonuses::addUnitBonus(const std::vector<Bonus> & bonus)
{ {
vstd::concatenate(bonusesToAdd, bonus); vstd::concatenate(bonusesToAdd, bonus);
treeVersionLocal++;
} }
void StackWithBonuses::updateUnitBonus(const std::vector<Bonus> & bonus) 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 //TODO: optimize, actualize to last value
vstd::concatenate(bonusesToUpdate, bonus); vstd::concatenate(bonusesToUpdate, bonus);
treeVersionLocal++;
} }
void StackWithBonuses::removeUnitBonus(const std::vector<Bonus> & bonus) 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(bonusesToAdd, [&](const Bonus & b){return selector(&b);});
vstd::erase_if(bonusesToUpdate, [&](const Bonus & b){return selector(&b);}); vstd::erase_if(bonusesToUpdate, [&](const Bonus & b){return selector(&b);});
treeVersionLocal++;
} }
std::string StackWithBonuses::getDescription() const std::string StackWithBonuses::getDescription() const
{ {
std::ostringstream oss; std::ostringstream oss;
oss << unitOwner().getStr(); oss << unitOwner().toString();
oss << " battle stack [" << unitId() << "]: " << getCount() << " of "; oss << " battle stack [" << unitId() << "]: " << getCount() << " of ";
if(type) if(type)
oss << type->getJsonKey(); oss << type->getJsonKey();
@ -256,7 +285,7 @@ std::shared_ptr<StackWithBonuses> HypotheticBattle::getForUpdate(uint32_t id)
if(iter == stackStates.end()) 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); auto ret = std::make_shared<StackWithBonuses>(this, s);
stackStates[id] = ret; stackStates[id] = ret;
@ -291,12 +320,17 @@ battle::Units HypotheticBattle::getUnitsIf(battle::UnitFilter predicate) const
return ret; return ret;
} }
BattleID HypotheticBattle::getBattleID() const
{
return subject->getBattle()->getBattleID();
}
int32_t HypotheticBattle::getActiveStackID() const int32_t HypotheticBattle::getActiveStackID() const
{ {
return activeUnitId; return activeUnitId;
} }
void HypotheticBattle::nextRound(int32_t roundNr) void HypotheticBattle::nextRound()
{ {
//TODO:HypotheticBattle::nextRound //TODO:HypotheticBattle::nextRound
for(auto unit : battleAliveUnits()) for(auto unit : battleAliveUnits())
@ -433,6 +467,24 @@ int64_t HypotheticBattle::getActualDamage(const DamageRange & damage, int32_t at
return (damage.min + damage.max) / 2; 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 int64_t HypotheticBattle::getTreeVersion() const
{ {
return getBonusBearer()->getTreeVersion() + bonusTreeVersion; return getBonusBearer()->getTreeVersion() + bonusTreeVersion;
@ -523,8 +575,9 @@ const Services * HypotheticBattle::HypotheticEnvironment::services() const
return env->services(); 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; return owner;
} }

View File

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

View File

@ -12,6 +12,7 @@
#include "../../lib/CRandomGenerator.h" #include "../../lib/CRandomGenerator.h"
#include "../../lib/CStack.h" #include "../../lib/CStack.h"
#include "../../lib/battle/BattleAction.h"
void CEmptyAI::saveGame(BinarySerializer & h, const int version) void CEmptyAI::saveGame(BinarySerializer & h, const int version)
{ {
@ -26,25 +27,26 @@ void CEmptyAI::initGameInterface(std::shared_ptr<Environment> ENV, std::shared_p
cb = CB; cb = CB;
env = ENV; env = ENV;
human=false; human=false;
playerID = *cb->getMyColor(); playerID = *cb->getPlayerID();
} }
void CEmptyAI::yourTurn() void CEmptyAI::yourTurn(QueryID queryID)
{ {
cb->selectionMade(0, queryID);
cb->endTurn(); cb->endTurn();
} }
void CEmptyAI::activeStack(const CStack * stack) void CEmptyAI::activeStack(const BattleID & battleID, const CStack * stack)
{ {
cb->battleMakeUnitAction(BattleAction::makeDefend(stack)); cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack));
} }
void CEmptyAI::yourTacticPhase(int distance) void CEmptyAI::yourTacticPhase(const BattleID & battleID, int distance)
{ {
cb->battleMakeTacticAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide())); cb->battleMakeTacticAction(battleID, BattleAction::makeEndOFTacticPhase(cb->getBattle(battleID)->battleGetTacticsSide()));
} }
void CEmptyAI::heroGotLevel(const CGHeroInstance *hero, PrimarySkill::PrimarySkill pskill, std::vector<SecondarySkill> &skills, QueryID queryID) void CEmptyAI::heroGotLevel(const CGHeroInstance *hero, PrimarySkill pskill, std::vector<SecondarySkill> &skills, QueryID queryID)
{ {
cb->selectionMade(CRandomGenerator::getDefault().nextInt((int)skills.size() - 1), queryID); cb->selectionMade(CRandomGenerator::getDefault().nextInt((int)skills.size() - 1), queryID);
} }
@ -59,7 +61,7 @@ void CEmptyAI::showBlockingDialog(const std::string &text, const std::vector<Com
cb->selectionMade(0, askID); cb->selectionMade(0, askID);
} }
void CEmptyAI::showTeleportDialog(TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) void CEmptyAI::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID)
{ {
cb->selectionMade(0, askID); cb->selectionMade(0, askID);
} }
@ -73,3 +75,8 @@ void CEmptyAI::showMapObjectSelectDialog(QueryID askID, const Component & icon,
{ {
cb->selectionMade(0, askID); cb->selectionMade(0, askID);
} }
std::optional<BattleAction> CEmptyAI::makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState)
{
return std::nullopt;
}

View File

@ -23,15 +23,16 @@ public:
virtual void loadGame(BinaryDeserializer & 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 initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB) override;
void yourTurn() override; void yourTurn(QueryID queryID) override;
void yourTacticPhase(int distance) override; void yourTacticPhase(const BattleID & battleID, int distance) override;
void activeStack(const CStack * stack) override; void activeStack(const BattleID & battleID, const CStack * stack) override;
void heroGotLevel(const CGHeroInstance *hero, PrimarySkill::PrimarySkill pskill, std::vector<SecondarySkill> &skills, QueryID queryID) 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 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 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 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 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; 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" #define NAME "EmptyAI 0.1"

View File

@ -21,6 +21,7 @@
#include "../../lib/serializer/BinarySerializer.h" #include "../../lib/serializer/BinarySerializer.h"
#include "../../lib/serializer/BinaryDeserializer.h" #include "../../lib/serializer/BinaryDeserializer.h"
#include "../../lib/battle/BattleStateInfoForRetreat.h" #include "../../lib/battle/BattleStateInfoForRetreat.h"
#include "../../lib/battle/BattleInfo.h"
#include "AIGateway.h" #include "AIGateway.h"
#include "Goals/Goals.h" #include "Goals/Goals.h"
@ -34,26 +35,26 @@ const float RETREAT_THRESHOLD = 0.3f;
const double RETREAT_ABSOLUTE_THRESHOLD = 10000.; const double RETREAT_ABSOLUTE_THRESHOLD = 10000.;
//one thread may be turn of AI and another will be handling a side effect for AI2 //one thread may be turn of AI and another will be handling a side effect for AI2
boost::thread_specific_ptr<CCallback> cb; thread_local CCallback * cb = nullptr;
boost::thread_specific_ptr<AIGateway> ai; thread_local AIGateway * ai = nullptr;
//helper RAII to manage global ai/cb ptrs //helper RAII to manage global ai/cb ptrs
struct SetGlobalState struct SetGlobalState
{ {
SetGlobalState(AIGateway * AI) SetGlobalState(AIGateway * AI)
{ {
assert(!ai.get()); assert(!ai);
assert(!cb.get()); assert(!cb);
ai.reset(AI); ai = AI;
cb.reset(AI->myCb.get()); cb = AI->myCb.get();
} }
~SetGlobalState() ~SetGlobalState()
{ {
//TODO: how to handle rm? shouldn't be called after ai is destroyed, hopefully //TODO: how to handle rm? shouldn't be called after ai is destroyed, hopefully
//TODO: to ensure that, make rm unique_ptr //TODO: to ensure that, make rm unique_ptr
ai.release(); ai = nullptr;
cb.release(); cb = nullptr;
} }
}; };
@ -153,10 +154,13 @@ void AIGateway::artifactAssembled(const ArtifactLocation & al)
NET_EVENT_HANDLER; NET_EVENT_HANDLER;
} }
void AIGateway::showTavernWindow(const CGObjectInstance * townOrTavern) void AIGateway::showTavernWindow(const CGObjectInstance * object, const CGHeroInstance * visitor, QueryID queryID)
{ {
LOG_TRACE(logAi); LOG_TRACE(logAi);
NET_EVENT_HANDLER; NET_EVENT_HANDLER;
status.addQuery(queryID, "TavernWindow");
requestActionASAP([=](){ answerQuery(queryID, 0); });
} }
void AIGateway::showThievesGuildWindow(const CGObjectInstance * obj) 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()); LOG_TRACE_PARAMS(logAi, "victoryLossCheckResult '%s'", victoryLossCheckResult.messageToSelf.toString());
NET_EVENT_HANDLER; 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 // some whitespace to flush stream
logAi->debug(std::string(200, ' ')); logAi->debug(std::string(200, ' '));
@ -201,12 +205,12 @@ void AIGateway::gameOver(PlayerColor player, const EVictoryLossCheckResult & vic
{ {
if(victoryLossCheckResult.victory()) 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()); logAi->debug("Turn nr %d", myCb->getDate());
} }
else 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 // 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; 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); LOG_TRACE_PARAMS(logAi, "level '%i'", level);
NET_EVENT_HANDLER; NET_EVENT_HANDLER;
status.addQuery(queryID, "RecruitmentDialog");
requestActionASAP([=](){
recruitCreatures(dwelling, dst);
answerQuery(queryID, 0);
});
} }
void AIGateway::heroMovePointsChanged(const CGHeroInstance * hero) 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 //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 //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); LOG_TRACE(logAi);
NET_EVENT_HANDLER; NET_EVENT_HANDLER;
@ -387,7 +398,7 @@ void AIGateway::heroCreated(const CGHeroInstance * h)
NET_EVENT_HANDLER; 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); LOG_TRACE_PARAMS(logAi, "spellID '%i", spellID);
NET_EVENT_HANDLER; NET_EVENT_HANDLER;
@ -424,10 +435,13 @@ void AIGateway::receivedResource()
NET_EVENT_HANDLER; 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); LOG_TRACE(logAi);
NET_EVENT_HANDLER; NET_EVENT_HANDLER;
status.addQuery(queryID, "UniversityWindow");
requestActionASAP([=](){ answerQuery(queryID, 0); });
} }
void AIGateway::heroManaPointsChanged(const CGHeroInstance * hero) void AIGateway::heroManaPointsChanged(const CGHeroInstance * hero)
@ -497,10 +511,13 @@ void AIGateway::heroBonusChanged(const CGHeroInstance * hero, const Bonus & bonu
NET_EVENT_HANDLER; 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); LOG_TRACE(logAi);
NET_EVENT_HANDLER; NET_EVENT_HANDLER;
status.addQuery(queryID, "MarketWindow");
requestActionASAP([=](){ answerQuery(queryID, 0); });
} }
void AIGateway::showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) 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; 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); LOG_TRACE(logAi);
NET_EVENT_HANDLER; NET_EVENT_HANDLER;
@ -535,7 +552,7 @@ void AIGateway::initGameInterface(std::shared_ptr<Environment> env, std::shared_
cbc = CB; cbc = CB;
NET_EVENT_HANDLER; NET_EVENT_HANDLER;
playerID = *myCb->getMyColor(); playerID = *myCb->getPlayerID();
myCb->waitTillRealize = true; myCb->waitTillRealize = true;
myCb->unlockGsWhenWaiting = true; myCb->unlockGsWhenWaiting = true;
@ -544,15 +561,17 @@ void AIGateway::initGameInterface(std::shared_ptr<Environment> env, std::shared_
retrieveVisitableObjs(); retrieveVisitableObjs();
} }
void AIGateway::yourTurn() void AIGateway::yourTurn(QueryID queryID)
{ {
LOG_TRACE(logAi); LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID);
NET_EVENT_HANDLER; NET_EVENT_HANDLER;
status.addQuery(queryID, "YourTurn");
requestActionASAP([=](){ answerQuery(queryID, 0); });
status.startedTurn(); status.startedTurn();
makingTurn = std::make_unique<boost::thread>(&AIGateway::makeTurn, this); 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); LOG_TRACE_PARAMS(logAi, "queryID '%i'", queryID);
NET_EVENT_HANDLER; 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; NET_EVENT_HANDLER;
status.addQuery(askID, boost::str(boost::format("Teleport dialog query with %d exits") % exits.size())); 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); LOG_TRACE_PARAMS(logAi, "removableUnits '%i', queryID '%i'", removableUnits % queryID);
NET_EVENT_HANDLER; NET_EVENT_HANDLER;
std::string s1 = up ? up->nodeName() : "NONE"; std::string s1 = up->nodeName();
std::string s2 = down ? down->nodeName() : "NONE"; std::string s2 = down->nodeName();
status.addQuery(queryID, boost::str(boost::format("Garrison dialog with %s and %s") % s1 % s2)); 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([=]() requestActionASAP([=]()
{ {
if(removableUnits && up->tempOwner == down->tempOwner) if(removableUnits && up->tempOwner == down->tempOwner)
{
pickBestCreatures(down, up); pickBestCreatures(down, up);
}
answerQuery(queryID, 0); answerQuery(queryID, 0);
}); });
@ -757,7 +778,7 @@ bool AIGateway::makePossibleUpgrades(const CArmedInstance * obj)
{ {
UpgradeInfo ui; UpgradeInfo ui;
myCb->fillUpgradeInfo(obj, SlotID(i), 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]); myCb->upgradeCreature(obj, SlotID(i), ui.newID[0]);
upgraded = true; upgraded = true;
@ -773,8 +794,8 @@ void AIGateway::makeTurn()
{ {
MAKING_TURN; MAKING_TURN;
auto day = cb->getDate(Date::EDateType::DAY); auto day = cb->getDate(Date::DAY);
logAi->info("Player %d (%s) starting turn, day %d", playerID, playerID.getStr(), day); logAi->info("Player %d (%s) starting turn, day %d", playerID, playerID.toString(), day);
boost::shared_lock<boost::shared_mutex> gsLock(CGameState::mutex); boost::shared_lock<boost::shared_mutex> gsLock(CGameState::mutex);
setThreadName("AIGateway::makeTurn"); 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()); LOG_TRACE_PARAMS(logAi, "Hero %s and object %s at %s", h->getNameTranslated() % obj->getObjectName() % obj->pos.toString());
switch(obj->ID) switch(obj->ID)
{ {
case Obj::CREATURE_GENERATOR1:
recruitCreatures(dynamic_cast<const CGDwelling *>(obj), h.get());
break;
case Obj::TOWN: case Obj::TOWN:
if(h->visitedTown) //we are inside, not just attacking 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; NET_EVENT_HANDLER;
assert(playerID > PlayerColor::PLAYER_LIMIT || status.getBattle() == UPCOMING_BATTLE); assert(!playerID.isValidPlayer() || status.getBattle() == UPCOMING_BATTLE);
status.setBattle(ONGOING_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 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()); 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; NET_EVENT_HANDLER;
assert(status.getBattle() == ONGOING_BATTLE); assert(status.getBattle() == ONGOING_BATTLE);
status.setBattle(ENDING_BATTLE); status.setBattle(ENDING_BATTLE);
bool won = br->winner == myCb->battleGetMySide(); bool won = br->winner == myCb->getBattle(battleID)->battleGetMySide();
logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.getStr(), (won ? "won" : "lost"), battlename); logAi->debug("Player %d (%s): I %s the %s!", playerID, playerID.toString(), (won ? "won" : "lost"), battlename);
battlename.clear(); battlename.clear();
if (queryID != -1) if (queryID != QueryID::NONE)
{ {
status.addQuery(queryID, "Combat result dialog"); status.addQuery(queryID, "Combat result dialog");
const int confirmAction = 0; const int confirmAction = 0;
@ -1106,7 +1124,7 @@ void AIGateway::battleEnd(const BattleResult * br, QueryID queryID)
answerQuery(queryID, confirmAction); answerQuery(queryID, confirmAction);
}); });
} }
CAdventureAI::battleEnd(br, queryID); CAdventureAI::battleEnd(battleID, br, queryID);
} }
void AIGateway::waitTillFree() void AIGateway::waitTillFree()
@ -1419,7 +1437,7 @@ void AIGateway::tryRealize(Goals::Trade & g) //trade
void AIGateway::endTurn() 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()) if(!status.haveTurn())
{ {
logAi->error("Not having turn at the end of turn???"); 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 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) 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); boost::shared_lock<boost::shared_mutex> gsLock(CGameState::mutex);
whatToDo(); whatToDo();
}); });
newThread.detach();
} }
void AIGateway::lostHero(HeroPtr h) void AIGateway::lostHero(HeroPtr h)
@ -1605,7 +1625,7 @@ void AIStatus::waitTillFree()
{ {
boost::unique_lock<boost::mutex> lock(mx); boost::unique_lock<boost::mutex> lock(mx);
while(battle != NO_BATTLE || !remainingQueries.empty() || !objectsBeingVisited.empty() || ongoingHeroMovement) 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() bool AIStatus::haveTurn()

View File

@ -111,13 +111,13 @@ public:
std::string getBattleAIName() const override; std::string getBattleAIName() const override;
void initGameInterface(std::shared_ptr<Environment> env, std::shared_ptr<CCallback> CB) 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 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 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 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 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 saveGame(BinarySerializer & h, const int version) override; //saving
void loadGame(BinaryDeserializer & h, const int version) override; //loading void loadGame(BinaryDeserializer & h, const int version) override; //loading
@ -130,7 +130,7 @@ public:
void tileHidden(const std::unordered_set<int3> & pos) override; void tileHidden(const std::unordered_set<int3> & pos) override;
void artifactMoved(const ArtifactLocation & src, const ArtifactLocation & dst) override; void artifactMoved(const ArtifactLocation & src, const ArtifactLocation & dst) override;
void artifactAssembled(const ArtifactLocation & al) 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 showThievesGuildWindow(const CGObjectInstance * obj) override;
void playerBlocked(int reason, bool start) override; void playerBlocked(int reason, bool start) override;
void showPuzzleMap() override; void showPuzzleMap() override;
@ -144,20 +144,20 @@ public:
void heroVisitsTown(const CGHeroInstance * hero, const CGTownInstance * town) override; void heroVisitsTown(const CGHeroInstance * hero, const CGTownInstance * town) override;
void tileRevealed(const std::unordered_set<int3> & pos) override; void tileRevealed(const std::unordered_set<int3> & pos) override;
void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) override; void heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query) override;
void heroPrimarySkillChanged(const CGHeroInstance * hero, int which, si64 val) override; void heroPrimarySkillChanged(const CGHeroInstance * hero, PrimarySkill which, si64 val) override;
void showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level) override; void showRecruitmentDialog(const CGDwelling * dwelling, const CArmedInstance * dst, int level, QueryID queryID) override;
void heroMovePointsChanged(const CGHeroInstance * hero) override; void heroMovePointsChanged(const CGHeroInstance * hero) override;
void garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) override; void garrisonsChanged(ObjectInstanceID id1, ObjectInstanceID id2) override;
void newObject(const CGObjectInstance * obj) override; void newObject(const CGObjectInstance * obj) override;
void showHillFortWindow(const CGObjectInstance * object, const CGHeroInstance * visitor) override; void showHillFortWindow(const CGObjectInstance * object, const CGHeroInstance * visitor) override;
void playerBonusChanged(const Bonus & bonus, bool gain) override; void playerBonusChanged(const Bonus & bonus, bool gain) override;
void heroCreated(const CGHeroInstance *) 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 showInfoDialog(EInfoWindowMode type, const std::string & text, const std::vector<Component> & components, int soundID) override;
void requestRealized(PackageApplied * pa) override; void requestRealized(PackageApplied * pa) override;
void receivedResource() override; void receivedResource() override;
void objectRemoved(const CGObjectInstance * obj) override; void objectRemoved(const CGObjectInstance * obj, const PlayerColor & initiator) override;
void showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor) override; void showUniversityWindow(const IMarket * market, const CGHeroInstance * visitor, QueryID queryID) override;
void heroManaPointsChanged(const CGHeroInstance * hero) override; void heroManaPointsChanged(const CGHeroInstance * hero) override;
void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) override; void heroSecondarySkillChanged(const CGHeroInstance * hero, int which, int val) override;
void battleResultsApplied() override; void battleResultsApplied() override;
@ -165,12 +165,12 @@ public:
void objectPropertyChanged(const SetObjectProperty * sop) override; void objectPropertyChanged(const SetObjectProperty * sop) override;
void buildChanged(const CGTownInstance * town, BuildingID buildingID, int what) override; void buildChanged(const CGTownInstance * town, BuildingID buildingID, int what) override;
void heroBonusChanged(const CGHeroInstance * hero, const Bonus & bonus, bool gain) 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; 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 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 BattleResult * br, QueryID queryID) override; void battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID) override;
void makeTurn(); void makeTurn();

View File

@ -25,10 +25,6 @@
namespace NKAI namespace NKAI
{ {
extern boost::thread_specific_ptr<AIGateway> ai;
//extern static const int3 dirs[8];
const CGObjectInstance * ObjectIdRef::operator->() const const CGObjectInstance * ObjectIdRef::operator->() const
{ {
return cb->getObj(id, false); return cb->getObj(id, false);
@ -245,7 +241,7 @@ bool isObjectPassable(const CGObjectInstance * obj)
} }
// Pathfinder internal helper // 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) if((obj->ID == Obj::GARRISON || obj->ID == Obj::GARRISON2)
&& objectRelations != PlayerRelations::ENEMIES) && objectRelations != PlayerRelations::ENEMIES)
@ -278,7 +274,7 @@ creInfo infoFromDC(const dwellingContent & dc)
creInfo ci; creInfo ci;
ci.count = dc.first; ci.count = dc.first;
ci.creID = dc.second.size() ? dc.second.back() : CreatureID(-1); //should never be accessed 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.cre = VLC->creatures()->getById(ci.creID);
ci.level = ci.cre->getLevel(); //this is creature tier, while tryRealize expects dwelling level. Ignore. 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 namespace NKAI
{ {
struct creInfo; struct creInfo;
class AIGateway;
class Nullkiller; class Nullkiller;
const int GOLD_MINE_PRODUCTION = 1000, WOOD_ORE_MINE_PRODUCTION = 2, RESOURCE_MINE_PRODUCTION = 1; 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 float SAFE_ATTACK_CONSTANT;
extern const int GOLD_RESERVE; extern const int GOLD_RESERVE;
extern boost::thread_specific_ptr<CCallback> cb; extern thread_local CCallback * cb;
extern thread_local AIGateway * ai;
enum HeroRole enum HeroRole
{ {
@ -149,7 +151,7 @@ struct ObjectIdRef
} }
}; };
template<int id> template<Obj::Type id>
bool objWithID(const CGObjectInstance * obj) bool objWithID(const CGObjectInstance * obj)
{ {
return obj->ID == id; return obj->ID == id;
@ -201,7 +203,7 @@ void foreach_tile_pos(CCallback * cbp, const Func & foo) // avoid costly retriev
template<class Func> template<class Func>
void foreach_neighbour(const int3 & pos, const Func & foo) 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()) for(const int3 & dir : int3::getDirs())
{ {
const int3 n = pos + dir; 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 canBeEmbarkmentPoint(const TerrainTile * t, bool fromWater);
bool isObjectPassable(const CGObjectInstance * obj); bool isObjectPassable(const CGObjectInstance * obj);
bool isObjectPassable(const Nullkiller * ai, 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 isBlockVisitObj(const int3 & pos);
bool isWeeklyRevisitable(const CGObjectInstance * obj); bool isWeeklyRevisitable(const CGObjectInstance * obj);

View File

@ -153,7 +153,7 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
for(auto bonus : *bonusModifiers) for(auto bonus : *bonusModifiers)
{ {
// army bonuses will change and object bonuses are temporary // 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)); newArmyInstance.addNewBonus(std::make_shared<Bonus>(*bonus));
} }
@ -225,7 +225,8 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
if(weakest->count == 1) if(weakest->count == 1)
{ {
assert(resultingArmy.size() > 1); if (resultingArmy.size() == 1)
logAi->warn("Unexpected resulting army size!");
resultingArmy.erase(weakest); resultingArmy.erase(weakest);
} }
@ -255,7 +256,7 @@ std::shared_ptr<CCreatureSet> ArmyManager::getArmyAvailableToBuyAsCCreatureSet(
{ {
auto ci = infoFromDC(dwelling->creatures[i]); auto ci = infoFromDC(dwelling->creatures[i]);
if(!ci.count || ci.creID == -1) if(!ci.count || ci.creID == CreatureID::NONE)
continue; continue;
vstd::amin(ci.count, availableRes / ci.cre->getFullRecruitCost()); //max count we can afford 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]); auto ci = infoFromDC(dwelling->creatures[i]);
if(ci.creID == -1) continue; if(ci.creID == CreatureID::NONE) continue;
if(i < GameConstants::CREATURES_PER_TOWN && countGrowth) 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.creature = army.first.toCreature();
army.second.power = evaluateStackPower(army.second.creature, army.second.count); 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) for(auto &pair : townInfo->buildings)
{ {
if(pair.second->upgrade != -1) if(pair.second->upgrade != BuildingID::NONE)
{ {
parentMap[pair.second->upgrade] = pair.first; parentMap[pair.second->upgrade] = pair.first;
} }
@ -160,7 +160,7 @@ void BuildAnalyzer::update()
updateDailyIncome(); updateDailyIncome();
if(ai->cb->getDate(Date::EDateType::DAY) == 1) if(ai->cb->getDate(Date::DAY) == 1)
{ {
goldPreasure = 1; goldPreasure = 1;
} }
@ -256,7 +256,7 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
{ {
logAi->trace("cant build. Need other dwelling"); logAi->trace("cant build. Need other dwelling");
} }
else else if(missingBuildings[0] != toBuild)
{ {
logAi->trace("cant build. Need %d", missingBuildings[0].num); logAi->trace("cant build. Need %d", missingBuildings[0].num);
@ -274,6 +274,12 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
return prerequisite; return prerequisite;
} }
else
{
logAi->trace("Cant build. The building requires itself as prerequisite");
return info;
}
} }
} }
else else

View File

@ -72,7 +72,13 @@ void DangerHitMapAnalyzer::updateHitMap()
if(ai->cb->getPlayerRelations(ai->playerID, pair.first) != PlayerRelations::ENEMIES) if(ai->cb->getPlayerRelations(ai->playerID, pair.first) != PlayerRelations::ENEMIES)
continue; 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(); 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 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 secondarySkillBonus = Selector::targetSourceType()(BonusSource::SECONDARY_SKILL);
auto specialSecondarySkillBonuses = hero->getBonuses(heroSpecial.And(secondarySkillBonus)); auto specialSecondarySkillBonuses = hero->getBonuses(heroSpecial.And(secondarySkillBonus));
auto secondarySkillBonuses = hero->getBonuses(Selector::sourceTypeSel(BonusSource::SECONDARY_SKILL)); auto secondarySkillBonuses = hero->getBonuses(Selector::sourceTypeSel(BonusSource::SECONDARY_SKILL));
@ -83,7 +83,7 @@ float HeroManager::evaluateSpeciality(const CGHeroInstance * hero) const
if(hasBonus) if(hasBonus)
{ {
SecondarySkill bonusSkill = SecondarySkill(bonus->sid); SecondarySkill bonusSkill = bonus->sid.as<SecondarySkill>();
float bonusScore = wariorSkillsScores.evaluateSecSkill(hero, bonusSkill); float bonusScore = wariorSkillsScores.evaluateSecSkill(hero, bonusSkill);
if(bonusScore > 0) if(bonusScore > 0)
@ -190,6 +190,41 @@ bool HeroManager::heroCapReached() const
|| heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP); || 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 bool HeroManager::canRecruitHero(const CGTownInstance * town) const
{ {
if(!town) if(!town)
@ -278,7 +313,7 @@ void ExistingSkillRule::evaluateScore(const CGHeroInstance * hero, SecondarySkil
if(heroSkill.first == skill) if(heroSkill.first == skill)
return; return;
upgradesLeft += SecSkillLevel::EXPERT - heroSkill.second; upgradesLeft += MasteryLevel::EXPERT - heroSkill.second;
} }
if(score >= 2 || (score >= 1 && upgradesLeft <= 1)) 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); auto wisdomLevel = hero->getSecSkillLevel(SecondarySkill::WISDOM);
if(hero->level > 10 && wisdomLevel == SecSkillLevel::NONE) if(hero->level > 10 && wisdomLevel == MasteryLevel::NONE)
score += 1.5; score += 1.5;
} }
@ -310,7 +345,7 @@ void AtLeastOneMagicRule::evaluateScore(const CGHeroInstance * hero, SecondarySk
bool heroHasAnyMagic = vstd::contains_if(magicSchools, [&](SecondarySkill skill) -> bool bool heroHasAnyMagic = vstd::contains_if(magicSchools, [&](SecondarySkill skill) -> bool
{ {
return hero->getSecSkillLevel(skill) > SecSkillLevel::NONE; return hero->getSecSkillLevel(skill) > MasteryLevel::NONE;
}); });
if(!heroHasAnyMagic) if(!heroHasAnyMagic)

View File

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

View File

@ -93,9 +93,14 @@ std::vector<std::shared_ptr<ObjectCluster>> ObjectClusterizer::getLockedClusters
const CGObjectInstance * ObjectClusterizer::getBlocker(const AIPath & path) const const CGObjectInstance * ObjectClusterizer::getBlocker(const AIPath & path) const
{ {
for(auto node = path.nodes.rbegin(); node != path.nodes.rend(); node++) for(auto node = path.nodes.rbegin(); node != path.nodes.rend(); node++)
{
std::vector<const CGObjectInstance *> blockers = {};
if(node->layer == EPathfindingLayer::LAND || node->layer == EPathfindingLayer::SAIL)
{ {
auto guardPos = ai->cb->getGuardingCreaturePosition(node->coord); auto guardPos = ai->cb->getGuardingCreaturePosition(node->coord);
auto blockers = ai->cb->getVisitableObjs(node->coord);
blockers = ai->cb->getVisitableObjs(node->coord);
if(guardPos.valid()) if(guardPos.valid())
{ {
@ -106,6 +111,7 @@ const CGObjectInstance * ObjectClusterizer::getBlocker(const AIPath & path) cons
blockers.insert(blockers.begin(), guard); blockers.insert(blockers.begin(), guard);
} }
} }
}
if(node->specialAction && node->actionIsBlocked) if(node->specialAction && node->actionIsBlocked)
{ {

View File

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

View File

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

View File

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

View File

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

View File

@ -25,9 +25,6 @@
namespace NKAI namespace NKAI
{ {
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
const float TREAT_IGNORE_RATIO = 2; const float TREAT_IGNORE_RATIO = 2;
using namespace Goals; using namespace Goals;
@ -114,7 +111,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa
if(ai->nullkiller->isHeroLocked(town->garrisonHero.get())) if(ai->nullkiller->isHeroLocked(town->garrisonHero.get()))
{ {
logAi->trace( 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->garrisonHero->getNameTranslated(),
town->getNameTranslated()); town->getNameTranslated());

View File

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

View File

@ -17,9 +17,6 @@
namespace NKAI namespace NKAI
{ {
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
using namespace Goals; using namespace Goals;
std::string RecruitHeroBehavior::toString() const 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; continue;
if(cb->getHeroesInfo().size() < cb->getTownsInfo().size() + 1 if(cb->getHeroesInfo().size() < cb->getTownsInfo().size() + 1

View File

@ -21,9 +21,6 @@
namespace NKAI namespace NKAI
{ {
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
using namespace Goals; using namespace Goals;
std::string StartupBehavior::toString() const 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/BuyArmyAction.cpp
Pathfinding/Actions/BoatActions.cpp Pathfinding/Actions/BoatActions.cpp
Pathfinding/Actions/TownPortalAction.cpp Pathfinding/Actions/TownPortalAction.cpp
Pathfinding/Actions/AdventureSpellCastMovementActions.cpp
Pathfinding/Rules/AILayerTransitionRule.cpp Pathfinding/Rules/AILayerTransitionRule.cpp
Pathfinding/Rules/AIMovementAfterDestinationRule.cpp Pathfinding/Rules/AIMovementAfterDestinationRule.cpp
Pathfinding/Rules/AIMovementToDestinationRule.cpp Pathfinding/Rules/AIMovementToDestinationRule.cpp
@ -34,6 +35,7 @@ set(Nullkiller_SRCS
Goals/ExecuteHeroChain.cpp Goals/ExecuteHeroChain.cpp
Goals/ExchangeSwapTownHeroes.cpp Goals/ExchangeSwapTownHeroes.cpp
Goals/CompleteQuest.cpp Goals/CompleteQuest.cpp
Goals/StayAtTown.cpp
Markers/ArmyUpgrade.cpp Markers/ArmyUpgrade.cpp
Markers/HeroExchange.cpp Markers/HeroExchange.cpp
Markers/UnlockCluster.cpp Markers/UnlockCluster.cpp
@ -52,6 +54,7 @@ set(Nullkiller_SRCS
Behaviors/BuildingBehavior.cpp Behaviors/BuildingBehavior.cpp
Behaviors/GatherArmyBehavior.cpp Behaviors/GatherArmyBehavior.cpp
Behaviors/ClusterBehavior.cpp Behaviors/ClusterBehavior.cpp
Behaviors/StayAtTownBehavior.cpp
Helpers/ArmyFormation.cpp Helpers/ArmyFormation.cpp
AIGateway.cpp AIGateway.cpp
) )
@ -69,6 +72,7 @@ set(Nullkiller_HEADERS
Pathfinding/Actions/BuyArmyAction.h Pathfinding/Actions/BuyArmyAction.h
Pathfinding/Actions/BoatActions.h Pathfinding/Actions/BoatActions.h
Pathfinding/Actions/TownPortalAction.h Pathfinding/Actions/TownPortalAction.h
Pathfinding/Actions/AdventureSpellCastMovementActions.h
Pathfinding/Rules/AILayerTransitionRule.h Pathfinding/Rules/AILayerTransitionRule.h
Pathfinding/Rules/AIMovementAfterDestinationRule.h Pathfinding/Rules/AIMovementAfterDestinationRule.h
Pathfinding/Rules/AIMovementToDestinationRule.h Pathfinding/Rules/AIMovementToDestinationRule.h
@ -97,6 +101,7 @@ set(Nullkiller_HEADERS
Goals/ExchangeSwapTownHeroes.h Goals/ExchangeSwapTownHeroes.h
Goals/CompleteQuest.h Goals/CompleteQuest.h
Goals/Goals.h Goals/Goals.h
Goals/StayAtTown.h
Markers/ArmyUpgrade.h Markers/ArmyUpgrade.h
Markers/HeroExchange.h Markers/HeroExchange.h
Markers/UnlockCluster.h Markers/UnlockCluster.h
@ -115,6 +120,7 @@ set(Nullkiller_HEADERS
Behaviors/BuildingBehavior.h Behaviors/BuildingBehavior.h
Behaviors/GatherArmyBehavior.h Behaviors/GatherArmyBehavior.h
Behaviors/ClusterBehavior.h Behaviors/ClusterBehavior.h
Behaviors/StayAtTownBehavior.h
Helpers/ArmyFormation.h Helpers/ArmyFormation.h
AIGateway.h AIGateway.h
) )

View File

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

View File

@ -20,8 +20,6 @@ namespace NKAI
#define MIN_AI_STRENGTH (0.5f) //lower when combat AI gets smarter #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 #define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us
extern boost::thread_specific_ptr<AIGateway> ai;
engineBase::engineBase() engineBase::engineBase()
{ {
rules = new fl::RuleBlock(); rules = new fl::RuleBlock();

View File

@ -8,7 +8,11 @@
* *
*/ */
#pragma once #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" #include "../Goals/AbstractGoal.h"
VCMI_LIB_NAMESPACE_BEGIN VCMI_LIB_NAMESPACE_BEGIN

View File

@ -111,7 +111,7 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj)
{ {
auto cb = ai->cb.get(); 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; return 0;
switch(obj->ID) switch(obj->ID)

View File

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

View File

@ -22,6 +22,7 @@
#include "../../../lib/filesystem/Filesystem.h" #include "../../../lib/filesystem/Filesystem.h"
#include "../Goals/ExecuteHeroChain.h" #include "../Goals/ExecuteHeroChain.h"
#include "../Goals/BuildThis.h" #include "../Goals/BuildThis.h"
#include "../Goals/StayAtTown.h"
#include "../Goals/ExchangeSwapTownHeroes.h" #include "../Goals/ExchangeSwapTownHeroes.h"
#include "../Goals/DismissHero.h" #include "../Goals/DismissHero.h"
#include "../Markers/UnlockCluster.h" #include "../Markers/UnlockCluster.h"
@ -68,7 +69,7 @@ PriorityEvaluator::~PriorityEvaluator()
void PriorityEvaluator::initVisitTile() 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); std::string str = std::string((char *)file.first.get(), file.second);
engine = fl::FllImporter().fromString(str); engine = fl::FllImporter().fromString(str);
armyLossPersentageVariable = engine->getInputVariable("armyLoss"); armyLossPersentageVariable = engine->getInputVariable("armyLoss");
@ -241,13 +242,13 @@ uint64_t evaluateArtifactArmyValue(CArtifactInstance * art)
return 1500; return 1500;
auto statsValue = auto statsValue =
10 * art->valOfBonuses(BonusType::MOVEMENT, 1) 10 * art->valOfBonuses(BonusType::MOVEMENT, BonusCustomSubtype::heroMovementLand)
+ 1200 * art->valOfBonuses(BonusType::STACKS_SPEED) + 1200 * art->valOfBonuses(BonusType::STACKS_SPEED)
+ 700 * art->valOfBonuses(BonusType::MORALE) + 700 * art->valOfBonuses(BonusType::MORALE)
+ 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, PrimarySkill::ATTACK) + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::ATTACK))
+ 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, PrimarySkill::DEFENSE) + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::DEFENSE))
+ 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, PrimarySkill::KNOWLEDGE) + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::KNOWLEDGE))
+ 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, PrimarySkill::SPELL_POWER) + 700 * art->valOfBonuses(BonusType::PRIMARY_SKILL, BonusSubtypeID(PrimarySkill::SPELL_POWER))
+ 500 * art->valOfBonuses(BonusType::LUCK); + 500 * art->valOfBonuses(BonusType::LUCK);
auto classValue = 0; auto classValue = 0;
@ -309,6 +310,9 @@ uint64_t RewardEvaluator::getArmyReward(
: 0; : 0;
case Obj::PANDORAS_BOX: case Obj::PANDORAS_BOX:
return 5000; return 5000;
case Obj::MAGIC_WELL:
case Obj::MAGIC_SPRING:
return getManaRecoveryArmyReward(hero);
default: default:
return 0; return 0;
} }
@ -450,6 +454,11 @@ uint64_t RewardEvaluator::townArmyGrowth(const CGTownInstance * town) const
return result; 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 float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) const
{ {
if(!target) 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)) if(!hut->wasVisited(hero->tempOwner))
return role == HeroRole::SCOUT ? 2 : 0; return role == HeroRole::SCOUT ? 2 : 0;
auto skill = SecondarySkill(hut->ability); if(hero->getSecSkillLevel(skill) != MasteryLevel::NONE
if(hero->getSecSkillLevel(skill) != SecSkillLevel::NONE
|| hero->secSkills.size() >= GameConstants::SKILL_PER_HERO) || hero->secSkills.size() >= GameConstants::SKILL_PER_HERO)
return 0; return 0;
@ -566,7 +578,7 @@ float RewardEvaluator::getSkillReward(const CGObjectInstance * target, const CGH
case Obj::LIBRARY_OF_ENLIGHTENMENT: case Obj::LIBRARY_OF_ENLIGHTENMENT:
return 8; return 8;
case Obj::WITCH_HUT: case Obj::WITCH_HUT:
return evaluateWitchHutSkillScore(dynamic_cast<const CGWitchHut *>(target), hero, role); return evaluateWitchHutSkillScore(target, hero, role);
case Obj::PANDORAS_BOX: case Obj::PANDORAS_BOX:
//Can contains experience, spells, or skills (only on custom maps) //Can contains experience, spells, or skills (only on custom maps)
return 2.5f; 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) void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uint8_t turn, uint64_t ourStrength)
{ {
HitMapInfo enemyDanger = evaluationContext.evaluator.getEnemyHeroDanger(tile, turn); 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<DefendTownEvaluator>());
evaluationContextBuilders.push_back(std::make_shared<ExchangeSwapTownHeroesContextBuilder>()); evaluationContextBuilders.push_back(std::make_shared<ExchangeSwapTownHeroesContextBuilder>());
evaluationContextBuilders.push_back(std::make_shared<DismissHeroContextBuilder>(ai)); evaluationContextBuilders.push_back(std::make_shared<DismissHeroContextBuilder>(ai));
evaluationContextBuilders.push_back(std::make_shared<StayAtTownManaRecoveryEvaluator>());
} }
EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal) const EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal) const

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -17,9 +17,6 @@
namespace NKAI namespace NKAI
{ {
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
using namespace Goals; using namespace Goals;
bool isKeyMaster(const QuestInfo & q) bool isKeyMaster(const QuestInfo & q)
@ -41,41 +38,28 @@ TGoalVec CompleteQuest::decompose() const
logAi->debug("Trying to realize quest: %s", questToString()); logAi->debug("Trying to realize quest: %s", questToString());
switch(q.quest->missionType) if(!q.quest->mission.artifacts.empty())
{
case CQuest::MISSION_ART:
return missionArt(); return missionArt();
case CQuest::MISSION_HERO: if(!q.quest->mission.heroes.empty())
return missionHero(); return missionHero();
case CQuest::MISSION_ARMY: if(!q.quest->mission.creatures.empty())
return missionArmy(); return missionArmy();
case CQuest::MISSION_RESOURCES: if(q.quest->mission.resources.nonZero())
return missionResources(); return missionResources();
case CQuest::MISSION_KILL_HERO: if(q.quest->killTarget != ObjectInstanceID::NONE)
case CQuest::MISSION_KILL_CREATURE:
return missionDestroyObj(); return missionDestroyObj();
case CQuest::MISSION_PRIMARY_STAT: for(auto & s : q.quest->mission.primary)
if(s)
return missionIncreasePrimaryStat(); return missionIncreasePrimaryStat();
case CQuest::MISSION_LEVEL: if(q.quest->mission.heroLevel > 0)
return missionLevel(); 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(); return TGoalVec();
} }
@ -110,7 +94,7 @@ std::string CompleteQuest::questToString() const
return "find " + VLC->generaltexth->tentColors[q.obj->subID] + " keymaster tent"; 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"; return "inactive quest";
MetaString ms; MetaString ms;
@ -140,7 +124,7 @@ TGoalVec CompleteQuest::missionArt() const
CaptureObjectsBehavior findArts; CaptureObjectsBehavior findArts;
for(auto art : q.quest->m5arts) for(auto art : q.quest->mission.artifacts)
{ {
solutions.push_back(sptr(CaptureObjectsBehavior().ofType(Obj::ARTIFACT, art))); solutions.push_back(sptr(CaptureObjectsBehavior().ofType(Obj::ARTIFACT, art)));
} }
@ -226,7 +210,7 @@ TGoalVec CompleteQuest::missionResources() const
TGoalVec CompleteQuest::missionDestroyObj() const TGoalVec CompleteQuest::missionDestroyObj() const
{ {
auto obj = cb->getObjByQuestIdentifier(q.quest->m13489val); auto obj = cb->getObjByQuestIdentifier(q.quest->killTarget);
if(!obj) if(!obj)
return CaptureObjectsBehavior(q.obj).decompose(); return CaptureObjectsBehavior(q.obj).decompose();

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,9 +15,6 @@
namespace NKAI namespace NKAI
{ {
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
using namespace Goals; using namespace Goals;
bool SaveResources::operator==(const SaveResources & other) const 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 namespace NKAI
{ {
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
using namespace Goals; using namespace Goals;
ArmyUpgrade::ArmyUpgrade(const AIPath & upgradePath, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade) ArmyUpgrade::ArmyUpgrade(const AIPath & upgradePath, const CGObjectInstance * upgrader, const ArmyUpgradeInfo & upgrade)

View File

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

View File

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

View File

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

View File

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

View File

@ -44,6 +44,7 @@ namespace AIPathfinding
std::shared_ptr<AINodeStorage> nodeStorage) std::shared_ptr<AINodeStorage> nodeStorage)
:PathfinderConfig(nodeStorage, makeRuleset(cb, ai, nodeStorage)), aiNodeStorage(nodeStorage) :PathfinderConfig(nodeStorage, makeRuleset(cb, ai, nodeStorage)), aiNodeStorage(nodeStorage)
{ {
options.canUseCast = true;
} }
AIPathfinderConfig::~AIPathfinderConfig() = default; 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 namespace NKAI
{ {
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
namespace AIPathfinding namespace AIPathfinding
{ {
void BattleAction::execute(const CGHeroInstance * hero) const void BattleAction::execute(const CGHeroInstance * hero) const

View File

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

View File

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

View File

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

View File

@ -16,9 +16,6 @@
namespace NKAI namespace NKAI
{ {
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<AIGateway> ai;
namespace AIPathfinding namespace AIPathfinding
{ {
bool QuestAction::canAct(const AIPathNode * node) const 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 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); || questInfo.quest->checkQuest(node->actor->hero);
} }

View File

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

View File

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

View File

@ -10,6 +10,8 @@
#include "StdInc.h" #include "StdInc.h"
#include "AILayerTransitionRule.h" #include "AILayerTransitionRule.h"
#include "../../Engine/Nullkiller.h" #include "../../Engine/Nullkiller.h"
#include "../../../../lib/pathfinder/CPathfinder.h"
#include "../../../../lib/pathfinder/TurnInfo.h"
namespace NKAI namespace NKAI
{ {
@ -31,23 +33,79 @@ namespace AIPathfinding
if(!destination.blocked) if(!destination.blocked)
{ {
if(source.node->layer == EPathfindingLayer::LAND
&& (destination.node->layer == EPathfindingLayer::AIR || destination.node->layer == EPathfindingLayer::WATER))
{
if(pathfinderHelper->getTurnInfo()->isLayerAvailable(destination.node->layer))
return; return;
else
destination.blocked = true;
}
else
{
return;
}
} }
if(source.node->layer == EPathfindingLayer::LAND && destination.node->layer == EPathfindingLayer::SAIL) if(source.node->layer == EPathfindingLayer::LAND && destination.node->layer == EPathfindingLayer::SAIL)
{ {
std::shared_ptr<const VirtualBoatAction> virtualBoat = findVirtualBoat(destination, source); 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 #if NKAI_PATHFINDER_TRACE_LEVEL >= 1
logAi->trace("Embarking to virtual boat while moving %s -> %s!", source.coord.toString(), destination.coord.toString()); 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 #endif
} }
} }
} }
void AILayerTransitionRule::setup() 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; std::vector<const IShipyard *> shipyards;
@ -81,7 +139,7 @@ namespace AIPathfinding
auto summonBoatSpell = SpellID(SpellID::SUMMON_BOAT).toSpell(); auto summonBoatSpell = SpellID(SpellID::SUMMON_BOAT).toSpell();
if(hero->canCastThisSpell(summonBoatSpell) 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 // TODO: For lower school level we might need to check the existance of some boat
summonableVirtualBoats[hero] = std::make_shared<SummonBoatAction>(); summonableVirtualBoats[hero] = std::make_shared<SummonBoatAction>();
@ -113,30 +171,36 @@ namespace AIPathfinding
return virtualBoat; return virtualBoat;
} }
bool AILayerTransitionRule::tryEmbarkVirtualBoat( bool AILayerTransitionRule::tryUseSpecialAction(
CDestinationNodeInfo & destination, CDestinationNodeInfo & destination,
const PathNodeInfo & source, const PathNodeInfo & source,
std::shared_ptr<const VirtualBoatAction> virtualBoat) const std::shared_ptr<const SpecialAction> specialAction,
EPathNodeAction targetAction) const
{ {
bool result = false; bool result = false;
if(!specialAction->canAct(nodeStorage->getAINode(source.node)))
{
return false;
}
nodeStorage->updateAINode(destination.node, [&](AIPathNode * node) nodeStorage->updateAINode(destination.node, [&](AIPathNode * node)
{ {
auto boatNodeOptional = nodeStorage->getOrCreateNode( auto castNodeOptional = nodeStorage->getOrCreateNode(
node->coord, node->coord,
node->layer, node->layer,
virtualBoat->getActor(node->actor)); specialAction->getActor(node->actor));
if(boatNodeOptional) if(castNodeOptional)
{ {
AIPathNode * boatNode = boatNodeOptional.value(); AIPathNode * castNode = castNodeOptional.value();
if(boatNode->action == EPathNodeAction::UNKNOWN) if(castNode->action == EPathNodeAction::UNKNOWN)
{ {
boatNode->addSpecialAction(virtualBoat); castNode->addSpecialAction(specialAction);
destination.blocked = false; destination.blocked = false;
destination.action = EPathNodeAction::EMBARK; destination.action = targetAction;
destination.node = boatNode; destination.node = castNode;
result = true; result = true;
} }
else else

View File

@ -13,6 +13,7 @@
#include "../AINodeStorage.h" #include "../AINodeStorage.h"
#include "../../AIGateway.h" #include "../../AIGateway.h"
#include "../Actions/BoatActions.h" #include "../Actions/BoatActions.h"
#include "../Actions/AdventureSpellCastMovementActions.h"
#include "../../../../CCallback.h" #include "../../../../CCallback.h"
#include "../../../../lib/mapObjects/MapObjects.h" #include "../../../../lib/mapObjects/MapObjects.h"
#include "../../../../lib/pathfinder/PathfindingRules.h" #include "../../../../lib/pathfinder/PathfindingRules.h"
@ -29,6 +30,8 @@ namespace AIPathfinding
std::map<int3, std::shared_ptr<const BuildBoatAction>> virtualBoats; std::map<int3, std::shared_ptr<const BuildBoatAction>> virtualBoats;
std::shared_ptr<AINodeStorage> nodeStorage; std::shared_ptr<AINodeStorage> nodeStorage;
std::map<const CGHeroInstance *, std::shared_ptr<const SummonBoatAction>> summonableVirtualBoats; 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: public:
AILayerTransitionRule(CPlayerSpecificInfoCallback * cb, Nullkiller * ai, std::shared_ptr<AINodeStorage> nodeStorage); AILayerTransitionRule(CPlayerSpecificInfoCallback * cb, Nullkiller * ai, std::shared_ptr<AINodeStorage> nodeStorage);
@ -41,15 +44,17 @@ namespace AIPathfinding
private: private:
void setup(); void setup();
void collectVirtualBoats();
std::shared_ptr<const VirtualBoatAction> findVirtualBoat( std::shared_ptr<const VirtualBoatAction> findVirtualBoat(
CDestinationNodeInfo & destination, CDestinationNodeInfo & destination,
const PathNodeInfo & source) const; const PathNodeInfo & source) const;
bool tryEmbarkVirtualBoat( bool tryUseSpecialAction(
CDestinationNodeInfo & destination, CDestinationNodeInfo & destination,
const PathNodeInfo & source, 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); auto questInfo = QuestInfo(questObj->quest, destination.nodeObject, destination.coord);
QuestAction questAction(questInfo); 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; return false;
} }

View File

@ -13,6 +13,8 @@
#include "../../lib/CStack.h" #include "../../lib/CStack.h"
#include "../../CCallback.h" #include "../../CCallback.h"
#include "../../lib/CCreatureHandler.h" #include "../../lib/CCreatureHandler.h"
#include "../../lib/battle/BattleAction.h"
#include "../../lib/battle/BattleInfo.h"
static std::shared_ptr<CBattleCallback> cbc; static std::shared_ptr<CBattleCallback> cbc;
@ -47,12 +49,17 @@ void CStupidAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::share
CB->unlockGsWhenWaiting = false; CB->unlockGsWhenWaiting = false;
} }
void CStupidAI::actionFinished(const BattleAction &action) 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"); print("actionFinished called");
} }
void CStupidAI::actionStarted(const BattleAction &action) void CStupidAI::actionStarted(const BattleID & battleID, const BattleAction &action)
{ {
print("actionStarted called"); print("actionStarted called");
} }
@ -65,11 +72,11 @@ public:
std::vector<BattleHex> attackFrom; //for melee fight std::vector<BattleHex> attackFrom; //for melee fight
EnemyInfo(const CStack * _s) : s(_s), adi(0), adr(0) EnemyInfo(const CStack * _s) : s(_s), adi(0), adr(0)
{} {}
void calcDmg(const CStack * ourStack) void calcDmg(const BattleID & battleID, const CStack * ourStack)
{ {
// FIXME: provide distance info for Jousting bonus // FIXME: provide distance info for Jousting bonus
DamageEstimation retal; DamageEstimation retal;
DamageEstimation dmg = cbc->battleEstimateDamage(ourStack, s, 0, &retal); DamageEstimation dmg = cbc->getBattle(battleID)->battleEstimateDamage(ourStack, s, 0, &retal);
adi = static_cast<int>((dmg.damage.min + dmg.damage.max) / 2); adi = static_cast<int>((dmg.damage.min + dmg.damage.max) / 2);
adr = static_cast<int>((retal.damage.min + retal.damage.max) / 2); adr = static_cast<int>((retal.damage.min + retal.damage.max) / 2);
} }
@ -85,14 +92,14 @@ bool isMoreProfitable(const EnemyInfo &ei1, const EnemyInfo& ei2)
return (ei1.adi-ei1.adr) < (ei2.adi - ei2.adr); return (ei1.adi-ei1.adr) < (ei2.adi - ei2.adr);
} }
static bool willSecondHexBlockMoreEnemyShooters(const BattleHex &h1, const BattleHex &h2) static bool willSecondHexBlockMoreEnemyShooters(const BattleID & battleID, const BattleHex &h1, const BattleHex &h2)
{ {
int shooters[2] = {0}; //count of shooters on hexes int shooters[2] = {0}; //count of shooters on hexes
for(int i = 0; i < 2; i++) for(int i = 0; i < 2; i++)
{ {
for (auto & neighbour : (i ? h2 : h1).neighbouringTiles()) for (auto & neighbour : (i ? h2 : h1).neighbouringTiles())
if(const auto * s = cbc->battleGetUnitByPos(neighbour)) if(const auto * s = cbc->getBattle(battleID)->battleGetUnitByPos(neighbour))
if(s->isShooter()) if(s->isShooter())
shooters[i]++; shooters[i]++;
} }
@ -100,16 +107,16 @@ static bool willSecondHexBlockMoreEnemyShooters(const BattleHex &h1, const Battl
return shooters[0] < shooters[1]; return shooters[0] < shooters[1];
} }
void CStupidAI::yourTacticPhase(int distance) void CStupidAI::yourTacticPhase(const BattleID & battleID, int distance)
{ {
cb->battleMakeTacticAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide())); cb->battleMakeTacticAction(battleID, BattleAction::makeEndOFTacticPhase(cb->getBattle(battleID)->battleGetTacticsSide()));
} }
void CStupidAI::activeStack( const CStack * stack ) void CStupidAI::activeStack(const BattleID & battleID, const CStack * stack)
{ {
//boost::this_thread::sleep(boost::posix_time::seconds(2)); //boost::this_thread::sleep_for(boost::chrono::seconds(2));
print("activeStack called for " + stack->nodeName()); print("activeStack called for " + stack->nodeName());
ReachabilityInfo dists = cb->getReachability(stack); ReachabilityInfo dists = cb->getBattle(battleID)->getReachability(stack);
std::vector<EnemyInfo> enemiesShootable, enemiesReachable, enemiesUnreachable; std::vector<EnemyInfo> enemiesShootable, enemiesReachable, enemiesUnreachable;
if(stack->creatureId() == CreatureID::CATAPULT) if(stack->creatureId() == CreatureID::CATAPULT)
@ -122,24 +129,24 @@ void CStupidAI::activeStack( const CStack * stack )
attack.side = side; attack.side = side;
attack.stackNumber = stack->unitId(); attack.stackNumber = stack->unitId();
cb->battleMakeUnitAction(attack); cb->battleMakeUnitAction(battleID, attack);
return; return;
} }
else if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON)) else if(stack->hasBonusOfType(BonusType::SIEGE_WEAPON))
{ {
cb->battleMakeUnitAction(BattleAction::makeDefend(stack)); cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack));
return; return;
} }
for (const CStack *s : cb->battleGetStacks(CBattleCallback::ONLY_ENEMY)) for (const CStack *s : cb->getBattle(battleID)->battleGetStacks(CBattleInfoEssentials::ONLY_ENEMY))
{ {
if(cb->battleCanShoot(stack, s->getPosition())) if(cb->getBattle(battleID)->battleCanShoot(stack, s->getPosition()))
{ {
enemiesShootable.push_back(s); enemiesShootable.push_back(s);
} }
else else
{ {
std::vector<BattleHex> avHexes = cb->battleGetAvailableHexes(stack, false); std::vector<BattleHex> avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(stack, false);
for (BattleHex hex : avHexes) for (BattleHex hex : avHexes)
{ {
@ -162,21 +169,23 @@ void CStupidAI::activeStack( const CStack * stack )
} }
for ( auto & enemy : enemiesReachable ) for ( auto & enemy : enemiesReachable )
enemy.calcDmg( stack ); enemy.calcDmg(battleID, stack);
for ( auto & enemy : enemiesShootable ) for ( auto & enemy : enemiesShootable )
enemy.calcDmg( stack ); enemy.calcDmg(battleID, stack);
if(enemiesShootable.size()) if(enemiesShootable.size())
{ {
const EnemyInfo &ei= *std::max_element(enemiesShootable.begin(), enemiesShootable.end(), isMoreProfitable); const EnemyInfo &ei= *std::max_element(enemiesShootable.begin(), enemiesShootable.end(), isMoreProfitable);
cb->battleMakeUnitAction(BattleAction::makeShotAttack(stack, ei.s)); cb->battleMakeUnitAction(battleID, BattleAction::makeShotAttack(stack, ei.s));
return; return;
} }
else if(enemiesReachable.size()) else if(enemiesReachable.size())
{ {
const EnemyInfo &ei= *std::max_element(enemiesReachable.begin(), enemiesReachable.end(), &isMoreProfitable); 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))); 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; return;
} }
else if(enemiesUnreachable.size()) //due to #955 - a buggy battle may occur when there are no enemies else if(enemiesUnreachable.size()) //due to #955 - a buggy battle may occur when there are no enemies
@ -188,26 +197,26 @@ void CStupidAI::activeStack( const CStack * stack )
if(dists.distToNearestNeighbour(stack, closestEnemy->s) < GameConstants::BFIELD_SIZE) if(dists.distToNearestNeighbour(stack, closestEnemy->s) < GameConstants::BFIELD_SIZE)
{ {
cb->battleMakeUnitAction(goTowards(stack, closestEnemy->s->getAttackableHexes(stack))); cb->battleMakeUnitAction(battleID, goTowards(battleID, stack, closestEnemy->s->getAttackableHexes(stack)));
return; return;
} }
} }
cb->battleMakeUnitAction(BattleAction::makeDefend(stack)); cb->battleMakeUnitAction(battleID, BattleAction::makeDefend(stack));
return; return;
} }
void CStupidAI::battleAttack(const BattleAttack *ba) void CStupidAI::battleAttack(const BattleID & battleID, const BattleAttack *ba)
{ {
print("battleAttack called"); print("battleAttack called");
} }
void CStupidAI::battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged) void CStupidAI::battleStacksAttacked(const BattleID & battleID, const std::vector<BattleStackAttacked> & bsa, bool ranged)
{ {
print("battleStacksAttacked called"); print("battleStacksAttacked called");
} }
void CStupidAI::battleEnd(const BattleResult *br, QueryID queryID) void CStupidAI::battleEnd(const BattleID & battleID, const BattleResult *br, QueryID queryID)
{ {
print("battleEnd called"); print("battleEnd called");
} }
@ -217,38 +226,38 @@ void CStupidAI::battleEnd(const BattleResult *br, QueryID queryID)
// print("battleResultsApplied called"); // print("battleResultsApplied called");
// } // }
void CStupidAI::battleNewRoundFirst(int round) void CStupidAI::battleNewRoundFirst(const BattleID & battleID)
{ {
print("battleNewRoundFirst called"); print("battleNewRoundFirst called");
} }
void CStupidAI::battleNewRound(int round) void CStupidAI::battleNewRound(const BattleID & battleID)
{ {
print("battleNewRound called"); print("battleNewRound called");
} }
void CStupidAI::battleStackMoved(const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport) void CStupidAI::battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport)
{ {
print("battleStackMoved called"); print("battleStackMoved called");
} }
void CStupidAI::battleSpellCast(const BattleSpellCast *sc) void CStupidAI::battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc)
{ {
print("battleSpellCast called"); print("battleSpellCast called");
} }
void CStupidAI::battleStacksEffectsSet(const SetStackEffect & sse) void CStupidAI::battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse)
{ {
print("battleStacksEffectsSet called"); print("battleStacksEffectsSet called");
} }
void CStupidAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed) 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"); print("battleStart called");
side = Side; side = Side;
} }
void CStupidAI::battleCatapultAttacked(const CatapultAttack & ca) void CStupidAI::battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca)
{ {
print("battleCatapultAttacked called"); print("battleCatapultAttacked called");
} }
@ -258,10 +267,10 @@ void CStupidAI::print(const std::string &text) const
logAi->trace("CStupidAI [%p]: %s", this, text); logAi->trace("CStupidAI [%p]: %s", this, text);
} }
BattleAction CStupidAI::goTowards(const CStack * stack, std::vector<BattleHex> hexes) const BattleAction CStupidAI::goTowards(const BattleID & battleID, const CStack * stack, std::vector<BattleHex> hexes) const
{ {
auto reachability = cb->getReachability(stack); auto reachability = cb->getBattle(battleID)->getReachability(stack);
auto avHexes = cb->battleGetAvailableHexes(reachability, stack, false); auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false);
if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked
{ {

View File

@ -11,6 +11,7 @@
#include "../../lib/battle/BattleHex.h" #include "../../lib/battle/BattleHex.h"
#include "../../lib/battle/ReachabilityInfo.h" #include "../../lib/battle/ReachabilityInfo.h"
#include "../../lib/CGameInterface.h"
class EnemyInfo; class EnemyInfo;
@ -29,25 +30,27 @@ 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) override;
void actionFinished(const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero void initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB, AutocombatPreferences autocombatPreferences) override;
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 actionFinished(const BattleID & battleID, const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero
void battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa, bool ranged) override; //called when stack receives damage (after battleAttack()) void actionStarted(const BattleID & battleID, const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero
void battleEnd(const BattleResult *br, QueryID queryID) override; 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 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 battleNewRoundFirst(const BattleID & battleID) 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 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 CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport) override; void battleStackMoved(const BattleID & battleID, const CStack * stack, std::vector<BattleHex> dest, int distance, bool teleport) override;
void battleSpellCast(const BattleSpellCast *sc) override; void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) override;
void battleStacksEffectsSet(const SetStackEffect & sse) override;//called when a specific effect is set to stacks 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 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 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 CatapultAttack & ca) override; //called when catapult makes an attack void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override; //called when catapult makes an attack
private: private:
BattleAction goTowards(const CStack * stack, std::vector<BattleHex> hexes) const; BattleAction goTowards(const BattleID & battleID, const CStack * stack, std::vector<BattleHex> hexes) const;
}; };

View File

@ -21,12 +21,8 @@
#include "../../lib/mapObjects/CQuest.h" #include "../../lib/mapObjects/CQuest.h"
#include "../../lib/mapping/CMapDefines.h" #include "../../lib/mapping/CMapDefines.h"
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<VCAI> ai;
extern FuzzyHelper * fh; extern FuzzyHelper * fh;
//extern static const int3 dirs[8];
const CGObjectInstance * ObjectIdRef::operator->() const const CGObjectInstance * ObjectIdRef::operator->() const
{ {
return cb->getObj(id, false); return cb->getObj(id, false);
@ -228,7 +224,7 @@ creInfo infoFromDC(const dwellingContent & dc)
creInfo ci; creInfo ci;
ci.count = dc.first; ci.count = dc.first;
ci.creID = dc.second.size() ? dc.second.back() : CreatureID(-1); //should never be accessed 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.cre = VLC->creatures()->getById(ci.creID);
ci.level = ci.cre->getLevel(); //this is creature tier, while tryRealize expects dwelling level. Ignore. ci.level = ci.cre->getLevel(); //this is creature tier, while tryRealize expects dwelling level. Ignore.

View File

@ -18,6 +18,7 @@
#include "../../lib/mapObjects/CGHeroInstance.h" #include "../../lib/mapObjects/CGHeroInstance.h"
#include "../../CCallback.h" #include "../../CCallback.h"
class VCAI;
class CCallback; class CCallback;
struct creInfo; struct creInfo;
@ -33,7 +34,8 @@ const int ALLOWED_ROAMING_HEROES = 8;
extern const double SAFE_ATTACK_CONSTANT; extern const double SAFE_ATTACK_CONSTANT;
extern const int GOLD_RESERVE; 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 //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* //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) bool objWithID(const CGObjectInstance * obj)
{ {
return obj->ID == id; return obj->ID == id;
@ -192,7 +194,7 @@ void foreach_tile_pos(CCallback * cbp, const Func & foo) // avoid costly retriev
template<class Func> template<class Func>
void foreach_neighbour(const int3 & pos, const Func & foo) 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()) for(const int3 & dir : int3::getDirs())
{ {
const int3 n = pos + dir; const int3 n = pos + dir;

View File

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

View File

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

View File

@ -18,9 +18,6 @@
#define MIN_AI_STRENGTH (0.5f) //lower when combat AI gets smarter #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 #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() engineBase::engineBase()
{ {
rules = new fl::RuleBlock(); rules = new fl::RuleBlock();

View File

@ -8,7 +8,11 @@
* *
*/ */
#pragma once #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" #include "Goals/AbstractGoal.h"
VCMI_LIB_NAMESPACE_BEGIN VCMI_LIB_NAMESPACE_BEGIN

View File

@ -23,9 +23,6 @@
FuzzyHelper * fh; FuzzyHelper * fh;
extern boost::thread_specific_ptr<VCAI> ai;
extern boost::thread_specific_ptr<CCallback> cb;
Goals::TSubgoal FuzzyHelper::chooseSolution(Goals::TGoalVec vec) Goals::TSubgoal FuzzyHelper::chooseSolution(Goals::TGoalVec vec)
{ {
if(vec.empty()) 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) 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) 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; 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; return 0;
switch(obj->ID) switch(obj->ID)

View File

@ -51,3 +51,5 @@ public:
ui64 evaluateDanger(crint3 tile, const CGHeroInstance * visitor, const VCAI * ai); ui64 evaluateDanger(crint3 tile, const CGHeroInstance * visitor, const VCAI * ai);
ui64 evaluateDanger(crint3 tile, const CGHeroInstance * visitor); ui64 evaluateDanger(crint3 tile, const CGHeroInstance * visitor);
}; };
extern FuzzyHelper * fh;

View File

@ -14,11 +14,7 @@
#include "../FuzzyHelper.h" #include "../FuzzyHelper.h"
#include "../ResourceManager.h" #include "../ResourceManager.h"
#include "../BuildingManager.h" #include "../BuildingManager.h"
#include "../../../lib/StringConstants.h" #include "../../../lib/constants/StringConstants.h"
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<VCAI> ai;
extern FuzzyHelper * fh;
using namespace Goals; using namespace Goals;

View File

@ -14,10 +14,6 @@
#include "../AIhelper.h" #include "../AIhelper.h"
#include "../../../lib/mapObjects/CGTownInstance.h" #include "../../../lib/mapObjects/CGTownInstance.h"
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<VCAI> ai;
extern FuzzyHelper * fh;
using namespace Goals; using namespace Goals;
bool AdventureSpellCast::operator==(const AdventureSpellCast & other) const bool AdventureSpellCast::operator==(const AdventureSpellCast & other) const

View File

@ -17,12 +17,7 @@
#include "../ResourceManager.h" #include "../ResourceManager.h"
#include "../BuildingManager.h" #include "../BuildingManager.h"
#include "../../../lib/mapObjects/CGTownInstance.h" #include "../../../lib/mapObjects/CGTownInstance.h"
#include "../../../lib/StringConstants.h" #include "../../../lib/constants/StringConstants.h"
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<VCAI> ai;
extern FuzzyHelper * fh;
using namespace Goals; using namespace Goals;

View File

@ -13,10 +13,6 @@
#include "../FuzzyHelper.h" #include "../FuzzyHelper.h"
#include "../AIhelper.h" #include "../AIhelper.h"
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<VCAI> ai;
extern FuzzyHelper * fh;
using namespace Goals; using namespace Goals;
bool BuildBoat::operator==(const BuildBoat & other) const bool BuildBoat::operator==(const BuildBoat & other) const

View File

@ -16,12 +16,7 @@
#include "../ResourceManager.h" #include "../ResourceManager.h"
#include "../BuildingManager.h" #include "../BuildingManager.h"
#include "../../../lib/mapObjects/CGTownInstance.h" #include "../../../lib/mapObjects/CGTownInstance.h"
#include "../../../lib/StringConstants.h" #include "../../../lib/constants/StringConstants.h"
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<VCAI> ai;
extern FuzzyHelper * fh;
using namespace Goals; using namespace Goals;

View File

@ -13,11 +13,6 @@
#include "../AIhelper.h" #include "../AIhelper.h"
#include "../../../lib/mapObjects/CGTownInstance.h" #include "../../../lib/mapObjects/CGTownInstance.h"
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<VCAI> ai;
extern FuzzyHelper * fh;
using namespace Goals; using namespace Goals;
bool BuyArmy::operator==(const BuyArmy & other) const bool BuyArmy::operator==(const BuyArmy & other) const

View File

@ -16,11 +16,6 @@
#include "../FuzzyHelper.h" #include "../FuzzyHelper.h"
#include "../AIhelper.h" #include "../AIhelper.h"
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<VCAI> ai;
extern FuzzyHelper * fh;
using namespace Goals; using namespace Goals;
bool ClearWayTo::operator==(const ClearWayTo & other) const bool ClearWayTo::operator==(const ClearWayTo & other) const

View File

@ -16,12 +16,7 @@
#include "../ResourceManager.h" #include "../ResourceManager.h"
#include "../BuildingManager.h" #include "../BuildingManager.h"
#include "../../../lib/mapObjects/CGMarket.h" #include "../../../lib/mapObjects/CGMarket.h"
#include "../../../lib/StringConstants.h" #include "../../../lib/constants/StringConstants.h"
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<VCAI> ai;
extern FuzzyHelper * fh;
using namespace Goals; using namespace Goals;
@ -65,14 +60,11 @@ TGoalVec CollectRes::getAllPossibleSubgoals()
return false; return false;
} }
break; break;
case Obj::WATER_WHEEL:
if (resID != GameResID(EGameResID::GOLD))
return false;
break;
case Obj::MYSTICAL_GARDEN: case Obj::MYSTICAL_GARDEN:
if ((resID != GameResID(EGameResID::GOLD)) && (resID != GameResID(EGameResID::GEMS))) if ((resID != GameResID(EGameResID::GOLD)) && (resID != GameResID(EGameResID::GEMS)))
return false; return false;
break; break;
case Obj::WATER_WHEEL:
case Obj::LEAN_TO: case Obj::LEAN_TO:
case Obj::WAGON: case Obj::WAGON:
if (resID != GameResID(EGameResID::GOLD)) if (resID != GameResID(EGameResID::GOLD))
@ -175,10 +167,10 @@ TSubgoal CollectRes::whatToDoToTrade()
int howManyCanWeBuy = 0; int howManyCanWeBuy = 0;
for (GameResID i = EGameResID::WOOD; i <= EGameResID::GOLD; ++i) for (GameResID i = EGameResID::WOOD; i <= EGameResID::GOLD; ++i)
{ {
if (GameResID(i) == resID) if (i.getNum() == resID)
continue; continue;
int toGive = -1, toReceive = -1; int toGive = -1, toReceive = -1;
m->getOffer(GameResID(i), resID, toGive, toReceive, EMarketMode::RESOURCE_RESOURCE); m->getOffer(i, resID, toGive, toReceive, EMarketMode::RESOURCE_RESOURCE);
assert(toGive > 0 && toReceive > 0); assert(toGive > 0 && toReceive > 0);
howManyCanWeBuy += toReceive * (ai->ah->freeResources()[i] / toGive); howManyCanWeBuy += toReceive * (ai->ah->freeResources()[i] / toGive);
} }

View File

@ -14,10 +14,6 @@
#include "../AIhelper.h" #include "../AIhelper.h"
#include "../../../lib/mapObjects/CQuest.h" #include "../../../lib/mapObjects/CQuest.h"
extern boost::thread_specific_ptr<CCallback> cb;
extern boost::thread_specific_ptr<VCAI> ai;
extern FuzzyHelper * fh;
using namespace Goals; using namespace Goals;
bool CompleteQuest::operator==(const CompleteQuest & other) const bool CompleteQuest::operator==(const CompleteQuest & other) const
@ -25,48 +21,43 @@ bool CompleteQuest::operator==(const CompleteQuest & other) const
return q.quest->qid == other.q.quest->qid; return q.quest->qid == other.q.quest->qid;
} }
bool isKeyMaster(const QuestInfo & q)
{
return q.obj && (q.obj->ID == Obj::BORDER_GATE || q.obj->ID == Obj::BORDERGUARD);
}
TGoalVec CompleteQuest::getAllPossibleSubgoals() TGoalVec CompleteQuest::getAllPossibleSubgoals()
{ {
TGoalVec solutions; TGoalVec solutions;
if(q.quest->missionType && q.quest->progress != CQuest::COMPLETE) if(!q.quest->isCompleted)
{ {
logAi->debug("Trying to realize quest: %s", questToString()); logAi->debug("Trying to realize quest: %s", questToString());
switch(q.quest->missionType) if(isKeyMaster(q))
{
case CQuest::MISSION_ART:
return missionArt();
case CQuest::MISSION_HERO:
return missionHero();
case CQuest::MISSION_ARMY:
return missionArmy();
case CQuest::MISSION_RESOURCES:
return missionResources();
case CQuest::MISSION_KILL_HERO:
case CQuest::MISSION_KILL_CREATURE:
return missionDestroyObj();
case CQuest::MISSION_PRIMARY_STAT:
return missionIncreasePrimaryStat();
case CQuest::MISSION_LEVEL:
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(); return missionKeymaster();
} //end of switch if(!q.quest->mission.artifacts.empty())
return missionArt();
if(!q.quest->mission.heroes.empty())
return missionHero();
if(!q.quest->mission.creatures.empty())
return missionArmy();
if(q.quest->mission.resources.nonZero())
return missionResources();
if(q.quest->killTarget != ObjectInstanceID::NONE)
return missionDestroyObj();
for(auto & s : q.quest->mission.primary)
if(s)
return missionIncreasePrimaryStat();
if(q.quest->mission.heroLevel > 0)
return missionLevel();
} }
return TGoalVec(); return TGoalVec();
@ -74,7 +65,7 @@ TGoalVec CompleteQuest::getAllPossibleSubgoals()
TSubgoal CompleteQuest::whatToDoToAchieve() TSubgoal CompleteQuest::whatToDoToAchieve()
{ {
if(q.quest->missionType == CQuest::MISSION_NONE) if(q.quest->mission == Rewardable::Limiter{})
{ {
throw cannotFulfillGoalException("Can not complete inactive quest"); throw cannotFulfillGoalException("Can not complete inactive quest");
} }
@ -108,7 +99,7 @@ std::string CompleteQuest::completeMessage() const
std::string CompleteQuest::questToString() const std::string CompleteQuest::questToString() const
{ {
if(q.quest->missionType == CQuest::MISSION_NONE) if(q.quest->questName == CQuest::missionName(0))
return "inactive quest"; return "inactive quest";
MetaString ms; MetaString ms;
@ -141,7 +132,7 @@ TGoalVec CompleteQuest::missionArt() const
if(!solutions.empty()) if(!solutions.empty())
return solutions; return solutions;
for(auto art : q.quest->m5arts) for(auto art : q.quest->mission.artifacts)
{ {
solutions.push_back(sptr(GetArtOfType(art))); //TODO: transport? solutions.push_back(sptr(GetArtOfType(art))); //TODO: transport?
} }
@ -169,7 +160,7 @@ TGoalVec CompleteQuest::missionArmy() const
if(!solutions.empty()) if(!solutions.empty())
return solutions; return solutions;
for(auto creature : q.quest->m6creatures) for(auto creature : q.quest->mission.creatures)
{ {
solutions.push_back(sptr(GatherTroops(creature.type->getId(), creature.count))); solutions.push_back(sptr(GatherTroops(creature.type->getId(), creature.count)));
} }
@ -183,7 +174,7 @@ TGoalVec CompleteQuest::missionIncreasePrimaryStat() const
if(solutions.empty()) if(solutions.empty())
{ {
for(int i = 0; i < q.quest->m2stats.size(); ++i) for(int i = 0; i < q.quest->mission.primary.size(); ++i)
{ {
// TODO: library, school and other boost objects // TODO: library, school and other boost objects
logAi->debug("Don't know how to increase primary stat %d", i); logAi->debug("Don't know how to increase primary stat %d", i);
@ -199,7 +190,7 @@ TGoalVec CompleteQuest::missionLevel() const
if(solutions.empty()) if(solutions.empty())
{ {
logAi->debug("Don't know how to reach hero level %d", q.quest->m13489val); logAi->debug("Don't know how to reach hero level %d", q.quest->mission.heroLevel);
} }
return solutions; return solutions;
@ -231,10 +222,10 @@ TGoalVec CompleteQuest::missionResources() const
} }
else else
{ {
for(int i = 0; i < q.quest->m7resources.size(); ++i) for(int i = 0; i < q.quest->mission.resources.size(); ++i)
{ {
if(q.quest->m7resources[i]) if(q.quest->mission.resources[i])
solutions.push_back(sptr(CollectRes(static_cast<EGameResID>(i), q.quest->m7resources[i]))); solutions.push_back(sptr(CollectRes(static_cast<EGameResID>(i), q.quest->mission.resources[i])));
} }
} }
} }
@ -250,7 +241,7 @@ TGoalVec CompleteQuest::missionDestroyObj() const
{ {
TGoalVec solutions; TGoalVec solutions;
auto obj = cb->getObjByQuestIdentifier(q.quest->m13489val); auto obj = cb->getObjByQuestIdentifier(q.quest->killTarget);
if(!obj) if(!obj)
return ai->ah->howToVisitObj(q.obj); return ai->ah->howToVisitObj(q.obj);

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