1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-03-17 20:58:07 +02:00

Merge pull request #5114 from vcmi/beta

Merge beta -> master
This commit is contained in:
Ivan Savenko 2024-12-19 18:14:26 +02:00 committed by GitHub
commit 1cbe1229ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
1674 changed files with 79721 additions and 47717 deletions

View File

@ -15,6 +15,7 @@ Please attach game logs: `VCMI_client.txt`, `VCMI_server.txt` etc.
**To Reproduce**
Steps to reproduce the behavior:
1. Go to '...'
2. Click on '....'
3. Scroll down to '....'
@ -24,7 +25,7 @@ Steps to reproduce the behavior:
A clear and concise description of what you expected to happen.
**Actual behavior**
A clear description what is currently happening
A clear description what is currently happening
**Did it work earlier?**
If this something which worked well some time ago, please let us know about version where it works or at date when it worked.
@ -33,8 +34,9 @@ If this something which worked well some time ago, please let us know about vers
If applicable, add screenshots to help explain your problem.
**Version**
- OS: [e.g. Windows, macOS Intel, macOS ARM, Android, Linux, iOS]
- Version: [VCMI version]
- OS: [e.g. Windows, macOS Intel, macOS ARM, Android, Linux, iOS]
- Version: [VCMI version]
**Additional context**
Add any other context about the problem here.

View File

@ -3,7 +3,6 @@ name: VCMI
on:
push:
branches:
- features/*
- beta
- master
- develop
@ -22,84 +21,112 @@ jobs:
- platform: linux-qt6
os: ubuntu-24.04
test: 0
before_install: linux_qt6.sh
preset: linux-clang-test
- platform: linux
os: ubuntu-24.04
test: 1
before_install: linux_qt5.sh
preset: linux-gcc-test
- platform: linux
os: ubuntu-20.04
test: 0
before_install: linux_qt5.sh
preset: linux-gcc-debug
- platform: mac-intel
os: macos-13
test: 0
pack: 1
upload: 1
pack_type: Release
extension: dmg
before_install: macos.sh
preset: macos-conan-ninja-release
conan_profile: macos-intel
conan_prebuilts: dependencies-mac-intel
conan_options: --options with_apple_system_libs=True
artifact_platform: intel
- platform: mac-arm
os: macos-13
test: 0
pack: 1
upload: 1
pack_type: Release
extension: dmg
before_install: macos.sh
preset: macos-arm-conan-ninja-release
conan_profile: macos-arm
conan_prebuilts: dependencies-mac-arm
conan_options: --options with_apple_system_libs=True
artifact_platform: arm
- platform: ios
os: macos-13
test: 0
pack: 1
upload: 1
pack_type: Release
extension: ipa
before_install: macos.sh
preset: ios-release-conan-ccache
conan_profile: ios-arm64
conan_prebuilts: dependencies-ios
conan_options: --options with_apple_system_libs=True
- platform: msvc
- platform: msvc-x64
os: windows-latest
test: 0
pack: 1
upload: 1
pack_type: RelWithDebInfo
extension: exe
before_install: msvc.sh
preset: windows-msvc-release
- platform: msvc-x86
os: windows-latest
test: 0
pack: 1
pack_type: RelWithDebInfo
extension: exe
preset: windows-msvc-release
- platform: mingw
os: ubuntu-22.04
before_install: msvc.sh
preset: windows-msvc-release-x86
- platform: mingw_x86_64
os: ubuntu-24.04
test: 0
pack: 1
pack_type: Release
extension: exe
cpack_args: -D CPACK_NSIS_EXECUTABLE=`which makensis`
cmake_args: -G Ninja
before_install: mingw.sh
preset: windows-mingw-conan-linux
conan_profile: mingw64-linux.jinja
- platform: mingw-32
os: ubuntu-22.04
conan_prebuilts: dependencies-mingw-x86-64
- platform: mingw_x86
os: ubuntu-24.04
test: 0
pack: 1
pack_type: Release
extension: exe
cpack_args: -D CPACK_NSIS_EXECUTABLE=`which makensis`
cmake_args: -G Ninja
before_install: mingw.sh
preset: windows-mingw-conan-linux
conan_profile: mingw32-linux.jinja
conan_prebuilts: dependencies-mingw-x86
- platform: android-32
os: macos-14
os: ubuntu-24.04
upload: 1
extension: apk
preset: android-conan-ninja-release
conan_profile: android-32
conan_options: --conf tools.android:ndk_path=$ANDROID_NDK_ROOT
before_install: android.sh
conan_profile: android-32-ndk
conan_prebuilts: dependencies-android-armeabi-v7a
artifact_platform: armeabi-v7a
- platform: android-64
os: macos-14
os: ubuntu-24.04
upload: 1
extension: apk
preset: android-conan-ninja-release
conan_profile: android-64
conan_options: --conf tools.android:ndk_path=$ANDROID_NDK_ROOT
before_install: android.sh
conan_profile: android-64-ndk
conan_prebuilts: dependencies-android-arm64-v8a
artifact_platform: arm64-v8a
runs-on: ${{ matrix.os }}
defaults:
@ -107,15 +134,25 @@ jobs:
shell: bash
steps:
- uses: actions/checkout@v4
- name: Checkout repository
uses: actions/checkout@v4
with:
submodules: recursive
- name: Dependencies
run: source '${{github.workspace}}/CI/${{matrix.platform}}/before_install.sh'
- name: Prepare CI
if: "${{ matrix.before_install != '' }}"
run: source '${{github.workspace}}/CI/before_install/${{matrix.before_install}}'
env:
VCMI_BUILD_PLATFORM: x64
- name: Install Conan Dependencies
if: "${{ matrix.conan_prebuilts != '' }}"
run: source '${{github.workspace}}/CI/install_conan_dependencies.sh' '${{matrix.conan_prebuilts}}'
- name: Install vcpkg Dependencies
if: ${{ startsWith(matrix.platform, 'msvc') }}
run: source '${{github.workspace}}/CI/install_vcpkg_dependencies.sh' '${{matrix.platform}}'
# ensure the ccache for each PR is separate so they don't interfere with each other
# fall back to ccache of the vcmi/vcmi repo if no PR-specific ccache is found
- name: ccache for PRs
@ -146,20 +183,24 @@ jobs:
HEROES_3_DATA_PASSWORD: ${{ secrets.HEROES_3_DATA_PASSWORD }}
if: ${{ env.HEROES_3_DATA_PASSWORD != '' && matrix.test == 1 }}
run: |
wget --progress=dot:giga https://github.com/vcmi-mods/vcmi-test-data/releases/download/v1.0/h3_assets.zip
if [[ ${{github.repository_owner}} == vcmi ]]
then
data_url="https://github.com/vcmi-mods/vcmi-test-data/releases/download/v1.0/h3_assets.zip"
else
data_url="https://github.com/${{github.repository_owner}}/vcmi-test-data/releases/download/v1.0/h3_assets.zip"
fi
wget --progress=dot:giga "$data_url" -O h3_assets.zip
7za x h3_assets.zip -p$HEROES_3_DATA_PASSWORD
mkdir -p ~/.local/share/vcmi/
mv h3_assets/* ~/.local/share/vcmi/
- uses: actions/setup-python@v5
- name: Install Conan
if: "${{ matrix.conan_profile != '' }}"
with:
python-version: '3.10'
run: pipx install 'conan<2.0'
- name: Conan setup
- name: Install Conan profile
if: "${{ matrix.conan_profile != '' }}"
run: |
pip3 install 'conan<2.0'
conan profile new default --detect
conan install . \
--install-folder=conan-generated \
@ -171,7 +212,13 @@ jobs:
env:
GENERATE_ONLY_BUILT_CONFIG: 1
- uses: actions/setup-java@v4
# Workaround for gradle not discovering SDK that was installed via conan
- name: Find Android NDK
if: ${{ startsWith(matrix.platform, 'android') }}
run: sudo ln -s -T /home/runner/.conan/data/android-ndk/r25c/_/_/package/4db1be536558d833e52e862fd84d64d75c2b3656/bin /usr/local/lib/android/sdk/ndk/25.2.9519653
- name: Install Java
uses: actions/setup-java@v4
if: ${{ startsWith(matrix.platform, 'android') }}
with:
distribution: 'temurin'
@ -202,11 +249,11 @@ jobs:
elif [[ (${{matrix.preset}} == android-conan-ninja-release) && (${{github.ref}} != 'refs/heads/master') ]]
then
cmake -DENABLE_CCACHE:BOOL=ON -DANDROID_GRADLE_PROPERTIES="applicationIdSuffix=.daily;signingConfig=dailySigning;applicationLabel=VCMI daily" --preset ${{ matrix.preset }}
elif [[ ${{matrix.platform}} != msvc ]]
elif [[ ${{startsWith(matrix.platform, 'msvc') }} ]]
then
cmake -DENABLE_CCACHE:BOOL=ON --preset ${{ matrix.preset }}
else
cmake --preset ${{ matrix.preset }}
else
cmake -DENABLE_CCACHE:BOOL=ON --preset ${{ matrix.preset }}
fi
- name: Build
@ -236,10 +283,13 @@ jobs:
if: ${{ matrix.pack == 1 }}
run: |
cd '${{github.workspace}}/out/build/${{matrix.preset}}'
CPACK_PATH=`which -a cpack | grep -m1 -v -i chocolatey`
counter=0; until "$CPACK_PATH" -C ${{matrix.pack_type}} ${{ matrix.cpack_args }} || ((counter > 20)); do sleep 3; ((counter++)); done
test -f '${{github.workspace}}/CI/${{matrix.platform}}/post_pack.sh' \
&& '${{github.workspace}}/CI/${{matrix.platform}}/post_pack.sh' '${{github.workspace}}' "$(ls '${{ env.VCMI_PACKAGE_FILE_NAME }}'.*)"
# Workaround for CPack bug on macOS 13
counter=0
until cpack -C ${{matrix.pack_type}} || ((counter > 20)); do
sleep 3
((counter++))
done
rm -rf _CPack_Packages
- name: Artifacts
@ -247,6 +297,7 @@ jobs:
uses: actions/upload-artifact@v4
with:
name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }}
compression-level: 0
path: |
${{github.workspace}}/out/build/${{matrix.preset}}/${{ env.VCMI_PACKAGE_FILE_NAME }}.${{ matrix.extension }}
@ -262,32 +313,35 @@ jobs:
echo "ANDROID_APK_PATH=$ANDROID_APK_PATH" >> $GITHUB_ENV
echo "ANDROID_AAB_PATH=$ANDROID_AAB_PATH" >> $GITHUB_ENV
- name: Android apk artifacts
- name: Upload android apk artifacts
if: ${{ startsWith(matrix.platform, 'android') }}
uses: actions/upload-artifact@v4
with:
name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }}
compression-level: 0
path: |
${{ env.ANDROID_APK_PATH }}
- name: Android aab artifacts
if: ${{ startsWith(matrix.platform, 'android') }}
- name: Upload Android aab artifacts
if: ${{ startsWith(matrix.platform, 'android') && github.ref == 'refs/heads/master' }}
uses: actions/upload-artifact@v4
with:
name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} - aab
compression-level: 0
path: |
${{ env.ANDROID_AAB_PATH }}
- name: Symbols
if: ${{ matrix.platform == 'msvc' }}
- name: Upload debug symbols
if: ${{ startsWith(matrix.platform, 'msvc') }}
uses: actions/upload-artifact@v4
with:
name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} - symbols
compression-level: 9
path: |
${{github.workspace}}/**/*.pdb
- name: Upload build
if: ${{ (matrix.pack == 1 || startsWith(matrix.platform, 'android')) && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/beta' || github.ref == 'refs/heads/master' || startsWith(github.ref, 'refs/heads/features/')) && matrix.platform != 'msvc' && matrix.platform != 'mingw-32' }}
if: ${{ (matrix.upload == 1) && (github.ref == 'refs/heads/develop' || github.ref == 'refs/heads/beta' || github.ref == 'refs/heads/master') }}
continue-on-error: true
run: |
if [ -z '${{ env.ANDROID_APK_PATH }}' ] ; then
@ -337,19 +391,20 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
if: "${{ matrix.conan_profile != '' }}"
with:
python-version: '3.10'
- name: Ensure LF line endings
run: |
find . -path ./.git -prune -o -path ./AI/FuzzyLite -prune -o -path ./test/googletest \
-o -path ./osx -prune -o -type f \
-not -name '*.png' -and -not -name '*.vcxproj*' -and -not -name '*.props' -and -not -name '*.wav' -and -not -name '*.webm' -and -not -name '*.ico' -and -not -name '*.bat' -print0 | \
-not -name '*.png' -and -not -name '*.ttf' -and -not -name '*.wav' -and -not -name '*.webm' -and -not -name '*.ico' -and -not -name '*.bat' -print0 | \
{ ! xargs -0 grep -l -z -P '\r\n'; }
- name: Validate JSON
run: |
sudo apt install python3-jstyleson
python3 CI/linux-qt6/validate_json.py
python3 CI/validate_json.py
- name: Validate Markdown
uses: DavidAnson/markdownlint-cli2-action@v18
with:
config: 'CI/example.markdownlint-cli2.jsonc'
globs: '**/*.md'

1
.gitignore vendored
View File

@ -43,6 +43,7 @@ VCMI_VS11.sdf
*.ipch
VCMI_VS11.opensdf
.DS_Store
.directory
CMakeUserPresets.json
compile_commands.json
fuzzylite.pc

2
.gitmodules vendored
View File

@ -1,7 +1,7 @@
[submodule "test/googletest"]
path = test/googletest
url = https://github.com/google/googletest
branch = v1.13.x
branch = v1.15.x
[submodule "AI/FuzzyLite"]
path = AI/FuzzyLite
url = https://github.com/fuzzylite/fuzzylite.git

View File

@ -12,6 +12,10 @@
#include "../../lib/CStack.h" // TODO: remove
// Eventually only IBattleInfoCallback and battle::Unit should be used,
// CUnitState should be private and CStack should be removed completely
#include "../../lib/spells/CSpellHandler.h"
#include "../../lib/spells/ISpellMechanics.h"
#include "../../lib/spells/ObstacleCasterProxy.h"
#include "../../lib/battle/CObstacleInstance.h"
uint64_t averageDmg(const DamageRange & range)
{
@ -25,9 +29,64 @@ void DamageCache::cacheDamage(const battle::Unit * attacker, const battle::Unit
damageCache[attacker->unitId()][defender->unitId()] = static_cast<float>(damage) / attacker->getCount();
}
void DamageCache::buildDamageCache(std::shared_ptr<HypotheticBattle> hb, int side)
void DamageCache::buildObstacleDamageCache(std::shared_ptr<HypotheticBattle> hb, BattleSide side)
{
for(const auto & obst : hb->battleGetAllObstacles(side))
{
auto spellObstacle = dynamic_cast<const SpellCreatedObstacle *>(obst.get());
if(!spellObstacle || !obst->triggersEffects())
continue;
auto triggerAbility = VLC->spells()->getById(obst->getTrigger());
auto triggerIsNegative = triggerAbility->isNegative() || triggerAbility->isDamage();
if(!triggerIsNegative)
continue;
std::unique_ptr<spells::BattleCast> cast = nullptr;
std::unique_ptr<spells::ObstacleCasterProxy> caster = nullptr;
if(spellObstacle->obstacleType == SpellCreatedObstacle::EObstacleType::SPELL_CREATED)
{
const auto * hero = hb->battleGetFightingHero(spellObstacle->casterSide);
caster = std::make_unique<spells::ObstacleCasterProxy>(hb->getSidePlayer(spellObstacle->casterSide), hero, *spellObstacle);
cast = std::make_unique<spells::BattleCast>(spells::BattleCast(hb.get(), caster.get(), spells::Mode::PASSIVE, obst->getTrigger().toSpell()));
}
auto affectedHexes = obst->getAffectedTiles();
auto stacks = hb->battleGetUnitsIf([](const battle::Unit * u) -> bool {
return u->alive() && !u->isTurret() && u->getPosition().isValid();
});
auto inner = std::make_shared<HypotheticBattle>(hb->env, hb);
for(auto stack : stacks)
{
auto updated = inner->getForUpdate(stack->unitId());
spells::Target target;
target.push_back(spells::Destination(updated.get()));
if(cast)
cast->castEval(inner->getServerCallback(), target);
auto damageDealt = stack->getAvailableHealth() - updated->getAvailableHealth();
for(auto hex : affectedHexes)
{
obstacleDamage[hex][stack->unitId()] = damageDealt;
}
}
}
}
void DamageCache::buildDamageCache(std::shared_ptr<HypotheticBattle> hb, BattleSide side)
{
if(parent == nullptr)
{
buildObstacleDamageCache(hb, side);
}
auto stacks = hb->battleGetUnitsIf([=](const battle::Unit * u) -> bool
{
return u->isValidTarget();
@ -70,6 +129,23 @@ int64_t DamageCache::getDamage(const battle::Unit * attacker, const battle::Unit
return damageCache[attacker->unitId()][defender->unitId()] * attacker->getCount();
}
int64_t DamageCache::getObstacleDamage(BattleHex hex, const battle::Unit * defender)
{
if(parent)
return parent->getObstacleDamage(hex, defender);
auto damages = obstacleDamage.find(hex);
if(damages == obstacleDamage.end())
return 0;
auto damage = damages->second.find(defender->unitId());
return damage == damages->second.end()
? 0
: damage->second;
}
int64_t DamageCache::getOriginalDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr<CBattleInfoCallback> hb)
{
if(parent)
@ -93,6 +169,8 @@ int64_t DamageCache::getOriginalDamage(const battle::Unit * attacker, const batt
AttackPossibility::AttackPossibility(BattleHex from, BattleHex dest, const BattleAttackInfo & attack)
: from(from), dest(dest), attack(attack)
{
this->attack.attackerPos = from;
this->attack.defenderPos = dest;
}
float AttackPossibility::damageDiff() const
@ -199,6 +277,8 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(
if(attackInfo.shooting)
return 0;
std::set<uint32_t> checkedUnits;
auto attacker = attackInfo.attacker;
auto hexes = attacker->getSurroundingHexes(hex);
for(BattleHex tile : hexes)
@ -206,9 +286,13 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(
auto st = state->battleGetUnitByPos(tile, true);
if(!st || !state->battleMatchOwner(st, attacker))
continue;
if(vstd::contains(checkedUnits, st->unitId()))
continue;
if(!state->battleCanShoot(st))
continue;
checkedUnits.insert(st->unitId());
// FIXME: provide distance info for Jousting bonus
BattleAttackInfo rangeAttackInfo(st, attacker, 0, true);
rangeAttackInfo.defenderPos = hex;
@ -218,9 +302,10 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(
auto rangeDmg = state->battleEstimateDamage(rangeAttackInfo);
auto meleeDmg = state->battleEstimateDamage(meleeAttackInfo);
auto cachedDmg = damageCache.getOriginalDamage(st, attacker, state);
int64_t gain = averageDmg(rangeDmg.damage) - averageDmg(meleeDmg.damage) + 1;
res += gain;
res += gain * cachedDmg / std::max<uint64_t>(1, averageDmg(rangeDmg.damage));
}
return res;
@ -243,7 +328,7 @@ AttackPossibility AttackPossibility::evaluate(
std::vector<BattleHex> defenderHex;
if(attackInfo.shooting)
defenderHex = defender->getHexes();
defenderHex.push_back(defender->getPosition());
else
defenderHex = CStack::meleeAttackHexes(attacker, defender, hex);
@ -261,63 +346,114 @@ AttackPossibility AttackPossibility::evaluate(
if (!attackInfo.shooting)
ap.attackerState->setPosition(hex);
std::vector<const battle::Unit*> units;
std::vector<const battle::Unit *> defenderUnits;
std::vector<const battle::Unit *> retaliatedUnits = {attacker};
std::vector<const battle::Unit *> affectedUnits;
if (attackInfo.shooting)
units = state->getAttackedBattleUnits(attacker, defHex, true, BattleHex::INVALID);
defenderUnits = state->getAttackedBattleUnits(attacker, defender, defHex, true, hex, defender->getPosition());
else
units = state->getAttackedBattleUnits(attacker, defHex, false, hex);
// ensure the defender is also affected
bool addDefender = true;
for(auto unit : units)
{
if (unit->unitId() == defender->unitId())
defenderUnits = state->getAttackedBattleUnits(attacker, defender, defHex, false, hex, defender->getPosition());
retaliatedUnits = state->getAttackedBattleUnits(defender, attacker, hex, false, defender->getPosition(), hex);
// attacker can not melle-attack itself but still can hit that place where it was before moving
vstd::erase_if(defenderUnits, [attacker](const battle::Unit * u) -> bool { return u->unitId() == attacker->unitId(); });
if(!vstd::contains_if(retaliatedUnits, [attacker](const battle::Unit * u) -> bool { return u->unitId() == attacker->unitId(); }))
{
addDefender = false;
break;
retaliatedUnits.push_back(attacker);
}
auto obstacleDamage = damageCache.getObstacleDamage(hex, attacker);
if(obstacleDamage > 0)
{
ap.attackerDamageReduce += calculateDamageReduce(nullptr, attacker, obstacleDamage, damageCache, state);
ap.attackerState->damage(obstacleDamage);
}
}
if(addDefender)
units.push_back(defender);
for(auto u : units)
// ensure the defender is also affected
if(!vstd::contains_if(defenderUnits, [defender](const battle::Unit * u) -> bool { return u->unitId() == defender->unitId(); }))
{
if(!ap.attackerState->alive())
break;
defenderUnits.push_back(defender);
}
affectedUnits = defenderUnits;
vstd::concatenate(affectedUnits, retaliatedUnits);
logAi->trace("Attacked battle units count %d, %d->%d", affectedUnits.size(), hex.hex, defHex.hex);
std::map<uint32_t, std::shared_ptr<battle::CUnitState>> defenderStates;
for(auto u : affectedUnits)
{
if(u->unitId() == attacker->unitId())
continue;
auto defenderState = u->acquireState();
ap.affectedUnits.push_back(defenderState);
for(int i = 0; i < totalAttacks; i++)
ap.affectedUnits.push_back(defenderState);
defenderStates[u->unitId()] = defenderState;
}
for(int i = 0; i < totalAttacks; i++)
{
if(!ap.attackerState->alive() || !defenderStates[defender->unitId()]->alive())
break;
for(auto u : defenderUnits)
{
auto defenderState = defenderStates.at(u->unitId());
int64_t damageDealt;
int64_t damageReceived;
float defenderDamageReduce;
float attackerDamageReduce;
DamageEstimation retaliation;
auto attackDmg = state->battleEstimateDamage(ap.attack, &retaliation);
vstd::amin(attackDmg.damage.min, defenderState->getAvailableHealth());
vstd::amin(attackDmg.damage.max, defenderState->getAvailableHealth());
vstd::amin(retaliation.damage.min, ap.attackerState->getAvailableHealth());
vstd::amin(retaliation.damage.max, ap.attackerState->getAvailableHealth());
damageDealt = averageDmg(attackDmg.damage);
defenderDamageReduce = calculateDamageReduce(attacker, defender, damageDealt, damageCache, state);
vstd::amin(damageDealt, defenderState->getAvailableHealth());
defenderDamageReduce = calculateDamageReduce(attacker, u, damageDealt, damageCache, state);
ap.attackerState->afterAttack(attackInfo.shooting, false);
//FIXME: use ranged retaliation
damageReceived = 0;
attackerDamageReduce = 0;
if (!attackInfo.shooting && defenderState->ableToRetaliate() && !counterAttacksBlocked)
if (!attackInfo.shooting && u->unitId() == defender->unitId() && defenderState->ableToRetaliate() && !counterAttacksBlocked)
{
damageReceived = averageDmg(retaliation.damage);
attackerDamageReduce = calculateDamageReduce(defender, attacker, damageReceived, damageCache, state);
for(auto retaliated : retaliatedUnits)
{
if(retaliated->unitId() == attacker->unitId())
{
int64_t damageReceived = averageDmg(retaliation.damage);
vstd::amin(damageReceived, ap.attackerState->getAvailableHealth());
attackerDamageReduce = calculateDamageReduce(defender, retaliated, damageReceived, damageCache, state);
ap.attackerState->damage(damageReceived);
}
else
{
auto retaliationCollateral = state->battleEstimateDamage(defender, retaliated, 0);
int64_t damageReceived = averageDmg(retaliationCollateral.damage);
vstd::amin(damageReceived, retaliated->getAvailableHealth());
if(defender->unitSide() == retaliated->unitSide())
defenderDamageReduce += calculateDamageReduce(defender, retaliated, damageReceived, damageCache, state);
else
ap.collateralDamageReduce += calculateDamageReduce(defender, retaliated, damageReceived, damageCache, state);
defenderStates.at(retaliated->unitId())->damage(damageReceived);
}
}
defenderState->afterAttack(attackInfo.shooting, true);
}
@ -331,21 +467,30 @@ AttackPossibility AttackPossibility::evaluate(
if(attackerSide == u->unitSide())
ap.collateralDamageReduce += defenderDamageReduce;
if(u->unitId() == defender->unitId() ||
(!attackInfo.shooting && CStack::isMeleeAttackPossible(u, attacker, hex)))
if(u->unitId() == defender->unitId()
|| (!attackInfo.shooting && CStack::isMeleeAttackPossible(u, attacker, hex)))
{
//FIXME: handle RANGED_RETALIATION ?
ap.attackerDamageReduce += attackerDamageReduce;
}
ap.attackerState->damage(damageReceived);
defenderState->damage(damageDealt);
if (!ap.attackerState->alive() || !defenderState->alive())
break;
if(u->unitId() == defender->unitId())
{
ap.defenderDead = !defenderState->alive();
}
}
}
#if BATTLE_TRACE_LEVEL>=2
logAi->trace("BattleAI AP: %s -> %s at %d from %d, affects %d units: d:%lld a:%lld c:%lld s:%lld",
attackInfo.attacker->unitType()->getJsonKey(),
attackInfo.defender->unitType()->getJsonKey(),
(int)ap.dest, (int)ap.from, (int)ap.affectedUnits.size(),
ap.defenderDamageReduce, ap.attackerDamageReduce, ap.collateralDamageReduce, ap.shootersBlockedDmg);
#endif
if(!bestAp.dest.isValid() || ap.attackValue() > bestAp.attackValue())
bestAp = ap;
}

View File

@ -18,16 +18,20 @@ class DamageCache
{
private:
std::unordered_map<uint32_t, std::unordered_map<uint32_t, float>> damageCache;
std::map<BattleHex, std::unordered_map<uint32_t, int64_t>> obstacleDamage;
DamageCache * parent;
void buildObstacleDamageCache(std::shared_ptr<HypotheticBattle> hb, BattleSide side);
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 getObstacleDamage(BattleHex hex, const battle::Unit * defender);
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);
void buildDamageCache(std::shared_ptr<HypotheticBattle> hb, BattleSide side);
};
/// <summary>
@ -49,6 +53,7 @@ public:
float attackerDamageReduce = 0; //usually by counter-attack
float collateralDamageReduce = 0; // friendly fire (usually by two-hex attacks)
int64_t shootersBlockedDmg = 0;
bool defenderDead = false;
AttackPossibility(BattleHex from, BattleHex dest, const BattleAttackInfo & attack_);

View File

@ -1,106 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<CodeBlocks_project_file>
<FileVersion major="1" minor="6" />
<Project>
<Option title="BattleAI" />
<Option pch_mode="2" />
<Option compiler="gcc" />
<Build>
<Target title="Debug-win32">
<Option platforms="Windows;" />
<Option output="../BattleAI" imp_lib="$(TARGET_OUTPUT_DIR)$(TARGET_OUTPUT_BASENAME).a" def_file="$(TARGET_OUTPUT_DIR)$(TARGET_OUTPUT_BASENAME).def" prefix_auto="1" extension_auto="1" />
<Option object_output="obj/Debug/x86/" />
<Option type="3" />
<Option compiler="gcc" />
<Compiler>
<Add option="-g" />
</Compiler>
<Linker>
<Add option="-lboost_thread$(#boost.libsuffix32)" />
<Add option="-lboost_system$(#boost.libsuffix32)" />
<Add option="-lVCMI_lib" />
<Add directory="$(#boost.lib32)" />
</Linker>
</Target>
<Target title="Release-win32">
<Option platforms="Windows;" />
<Option output="../BattleAI" imp_lib="$(TARGET_OUTPUT_DIR)$(TARGET_OUTPUT_BASENAME).a" def_file="$(TARGET_OUTPUT_DIR)$(TARGET_OUTPUT_BASENAME).def" prefix_auto="1" extension_auto="1" />
<Option object_output="obj/Release/x86/" />
<Option type="3" />
<Option compiler="gcc" />
<Compiler>
<Add option="-O2" />
</Compiler>
<Linker>
<Add option="-s" />
<Add option="-lboost_thread$(#boost.libsuffix32)" />
<Add option="-lboost_system$(#boost.libsuffix32)" />
<Add option="-lVCMI_lib" />
<Add directory="$(#boost.lib32)" />
</Linker>
</Target>
<Target title="Debug-win64">
<Option platforms="Windows;" />
<Option output="../BattleAI" imp_lib="$(TARGET_OUTPUT_DIR)$(TARGET_OUTPUT_BASENAME).a" def_file="$(TARGET_OUTPUT_DIR)$(TARGET_OUTPUT_BASENAME).def" prefix_auto="1" extension_auto="1" />
<Option object_output="obj/Debug/x64/" />
<Option type="3" />
<Option compiler="gnu_gcc_compiler_x64" />
<Compiler>
<Add option="-g" />
</Compiler>
<Linker>
<Add option="-lboost_thread$(#boost.libsuffix64)" />
<Add option="-lboost_system$(#boost.libsuffix64)" />
<Add option="-lVCMI_lib" />
<Add directory="$(#boost.lib64)" />
</Linker>
</Target>
</Build>
<Compiler>
<Add option="-pedantic" />
<Add option="-Wextra" />
<Add option="-Wall" />
<Add option="-std=gnu++11" />
<Add option="-fexceptions" />
<Add option="-Wpointer-arith" />
<Add option="-Wno-switch" />
<Add option="-Wno-sign-compare" />
<Add option="-Wno-unused-parameter" />
<Add option="-Wno-overloaded-virtual" />
<Add option="-DBOOST_ALL_DYN_LINK" />
<Add option="-DBOOST_SYSTEM_NO_DEPRECATED" />
<Add option="-D_WIN32_WINNT=0x0600" />
<Add option="-D_WIN32" />
<Add directory="$(#boost.include)" />
<Add directory="../../include" />
</Compiler>
<Linker>
<Add directory="../.." />
</Linker>
<Unit filename="AttackPossibility.cpp" />
<Unit filename="AttackPossibility.h" />
<Unit filename="BattleAI.cpp" />
<Unit filename="BattleAI.h" />
<Unit filename="CMakeLists.txt" />
<Unit filename="EnemyInfo.cpp" />
<Unit filename="EnemyInfo.h" />
<Unit filename="PossibleSpellcast.cpp" />
<Unit filename="PossibleSpellcast.h" />
<Unit filename="PotentialTargets.cpp" />
<Unit filename="PotentialTargets.h" />
<Unit filename="StackWithBonuses.cpp" />
<Unit filename="StackWithBonuses.h" />
<Unit filename="StdInc.h">
<Option compile="1" />
<Option weight="0" />
</Unit>
<Unit filename="ThreatMap.cpp" />
<Unit filename="ThreatMap.h" />
<Unit filename="common.cpp" />
<Unit filename="common.h" />
<Unit filename="main.cpp" />
<Extensions>
<lib_finder disable_auto="1" />
</Extensions>
</Project>
</CodeBlocks_project_file>

View File

@ -23,15 +23,17 @@
#include "../../lib/battle/BattleAction.h"
#include "../../lib/battle/BattleStateInfoForRetreat.h"
#include "../../lib/battle/CObstacleInstance.h"
#include "../../lib/StartInfo.h"
#include "../../lib/CStack.h" // TODO: remove
// Eventually only IBattleInfoCallback and battle::Unit should be used,
// CUnitState should be private and CStack should be removed completely
#include "../../lib/logging/VisualLogger.h"
#define LOGL(text) print(text)
#define LOGFL(text, formattingEl) print(boost::str(boost::format(text) % formattingEl))
CBattleAI::CBattleAI()
: side(-1),
: side(BattleSide::NONE),
wasWaitingForRealize(false),
wasUnlockingGs(false)
{
@ -47,6 +49,17 @@ CBattleAI::~CBattleAI()
}
}
void logHexNumbers()
{
#if BATTLE_TRACE_LEVEL >= 1
logVisual->updateWithLock("hexes", [](IVisualLogBuilder & b)
{
for(BattleHex hex = BattleHex(0); hex < GameConstants::BFIELD_SIZE; hex = BattleHex(hex + 1))
b.addText(hex, std::to_string(hex.hex));
});
#endif
}
void CBattleAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB)
{
env = ENV;
@ -57,6 +70,8 @@ void CBattleAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::share
CB->waitTillRealize = false;
CB->unlockGsWhenWaiting = false;
movesSkippedByDefense = 0;
logHexNumbers();
}
void CBattleAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB, AutocombatPreferences autocombatPreferences)
@ -86,7 +101,7 @@ void CBattleAI::yourTacticPhase(const BattleID & battleID, int distance)
cb->battleMakeTacticAction(battleID, BattleAction::makeEndOFTacticPhase(cb->getBattle(battleID)->battleGetTacticsSide()));
}
static float getStrengthRatio(std::shared_ptr<CBattleInfoCallback> cb, int side)
static float getStrengthRatio(std::shared_ptr<CBattleInfoCallback> cb, BattleSide side)
{
auto stacks = cb->battleGetAllStacks();
auto our = 0;
@ -108,6 +123,11 @@ static float getStrengthRatio(std::shared_ptr<CBattleInfoCallback> cb, int side)
return enemy == 0 ? 1.0f : static_cast<float>(our) / enemy;
}
int getSimulationTurnsCount(const StartInfo * startInfo)
{
return startInfo->difficulty < 4 ? 2 : 10;
}
void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack )
{
LOG_TRACE_PARAMS(logAi, "stack: %s", stack->nodeName());
@ -140,18 +160,19 @@ void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack )
logAi->trace("Build evaluator and targets");
#endif
BattleEvaluator evaluator(env, cb, stack, playerID, battleID, side, getStrengthRatio(cb->getBattle(battleID), side));
BattleEvaluator evaluator(
env, cb, stack, playerID, battleID, side,
getStrengthRatio(cb->getBattle(battleID), side),
getSimulationTurnsCount(env->game()->getStartInfo()));
result = evaluator.selectStackAction(stack);
if(autobattlePreferences.enableSpellsUsage && !skipCastUntilNextBattle && evaluator.canCastSpell())
if(autobattlePreferences.enableSpellsUsage && evaluator.canCastSpell())
{
auto spelCasted = evaluator.attemptCastingSpell(stack);
if(spelCasted)
return;
skipCastUntilNextBattle = true;
}
logAi->trace("Spellcast attempt completed in %lld", timeElapsed(start));
@ -176,7 +197,7 @@ void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack )
movesSkippedByDefense = 0;
}
logAi->trace("BattleAI decission made in %lld", timeElapsed(start));
logAi->trace("BattleAI decision made in %lld", timeElapsed(start));
cb->battleMakeUnitAction(battleID, result);
}
@ -206,7 +227,7 @@ BattleAction CBattleAI::useCatapult(const BattleID & battleID, const CStack * st
{
auto wallState = cb->getBattle(battleID)->battleGetWallState(wallPart);
if(wallState == EWallState::REINFORCED || wallState == EWallState::INTACT || wallState == EWallState::DAMAGED)
if(wallState != EWallState::NONE && wallState != EWallState::DESTROYED)
{
targetHex = cb->getBattle(battleID)->wallPartToBattleHex(wallPart);
break;
@ -229,12 +250,10 @@ BattleAction CBattleAI::useCatapult(const BattleID & battleID, const CStack * st
return attack;
}
void CBattleAI::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed)
void CBattleAI::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, BattleSide Side, bool replayAllowed)
{
LOG_TRACE(logAi);
side = Side;
skipCastUntilNextBattle = false;
}
void CBattleAI::print(const std::string &text) const

View File

@ -27,7 +27,7 @@ struct CurrentOffensivePotential
std::map<const CStack *, PotentialTargets> ourAttacks;
std::map<const CStack *, PotentialTargets> enemyAttacks;
CurrentOffensivePotential(ui8 side)
CurrentOffensivePotential(BattleSide side)
{
for(auto stack : cbc->battleGetStacks())
{
@ -50,11 +50,11 @@ struct CurrentOffensivePotential
return ourPotential - enemyPotential;
}
};
*/ // These lines may be usefull but they are't used in the code.
*/ // These lines may be useful but they are't used in the code.
class CBattleAI : public CBattleGameInterface
{
int side;
BattleSide side;
std::shared_ptr<CBattleCallback> cb;
std::shared_ptr<Environment> env;
@ -62,7 +62,6 @@ class CBattleAI : public CBattleGameInterface
bool wasWaitingForRealize;
bool wasUnlockingGs;
int movesSkippedByDefense;
bool skipCastUntilNextBattle;
public:
CBattleAI();
@ -80,7 +79,7 @@ public:
BattleAction useCatapult(const BattleID & battleID, const CStack *stack);
BattleAction useHealingTent(const BattleID & battleID, const CStack *stack);
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 battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, BattleSide side, bool replayAllowed) override;
//void actionFinished(const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero
//void actionStarted(const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero
//void battleAttack(const BattleAttack *ba) override; //called when stack is performing attack
@ -93,7 +92,7 @@ public:
//void battleSpellCast(const BattleSpellCast *sc) override;
//void battleStacksEffectsSet(const SetStackEffect & sse) override;//called when a specific effect is set to stacks
//void battleTriggerEffect(const BattleTriggerEffect & bte) override;
//void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) 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, BattleSide 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
AutocombatPreferences autobattlePreferences = AutocombatPreferences();
};

View File

@ -1,168 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="RD|Win32">
<Configuration>RD</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="RD|x64">
<Configuration>RD</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{C0300513-E845-43B4-9A4F-E8817EAEF57C}</ProjectGuid>
<RootNamespace>BattleAI</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>MultiByte</CharacterSet>
<PlatformToolset>v140_xp</PlatformToolset>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>MultiByte</CharacterSet>
<PlatformToolset>v140_xp</PlatformToolset>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='RD|Win32'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>MultiByte</CharacterSet>
<PlatformToolset>v142</PlatformToolset>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='RD|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>MultiByte</CharacterSet>
<PlatformToolset>v140_xp</PlatformToolset>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="..\..\VCMI_global_debug.props" />
<Import Project="..\..\VCMI_global.props" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="..\..\VCMI_global_debug.props" />
<Import Project="..\..\VCMI_global.props" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='RD|Win32'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="..\..\VCMI_global_release.props" />
<Import Project="..\..\VCMI_global.props" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='RD|x64'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="..\..\VCMI_global_release.props" />
<Import Project="..\..\VCMI_global.props" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<OutDir>$(VCMI_Out)\AI\</OutDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<OutDir>$(VCMI_Out)\AI\</OutDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='RD|Win32'">
<OutDir>$(VCMI_Out)/AI</OutDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='RD|x64'">
<OutDir>$(VCMI_Out)\AI\</OutDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>StdInc.h</PrecompiledHeaderFile>
<AdditionalOptions>/Zm159 %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
<AdditionalDependencies>VCMI_lib.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalLibraryDirectories>..\..\..\libs;..\..;..</AdditionalLibraryDirectories>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>StdInc.h</PrecompiledHeaderFile>
<AdditionalOptions>/Zm159 %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
<AdditionalDependencies>VCMI_lib.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='RD|Win32'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>StdInc.h</PrecompiledHeaderFile>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
<Optimization>MaxSpeed</Optimization>
</ClCompile>
<Link>
<AdditionalDependencies>VCMI_lib.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalLibraryDirectories>$(VCMI_Out)</AdditionalLibraryDirectories>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='RD|x64'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>StdInc.h</PrecompiledHeaderFile>
<AdditionalOptions>/Zm159 %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
<AdditionalDependencies>VCMI_lib.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="AttackPossibility.cpp" />
<ClCompile Include="common.cpp" />
<ClCompile Include="EnemyInfo.cpp" />
<ClCompile Include="main.cpp" />
<ClCompile Include="PossibleSpellcast.cpp" />
<ClCompile Include="PotentialTargets.cpp" />
<ClCompile Include="StackWithBonuses.cpp" />
<ClCompile Include="StdInc.cpp">
<PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
<PrecompiledHeaderFile Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">StdInc.h</PrecompiledHeaderFile>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='RD|Win32'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='RD|x64'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="BattleAI.cpp" />
<ClCompile Include="ThreatMap.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="AttackPossibility.h" />
<ClInclude Include="common.h" />
<ClInclude Include="EnemyInfo.h" />
<ClInclude Include="PossibleSpellcast.h" />
<ClInclude Include="PotentialTargets.h" />
<ClInclude Include="StackWithBonuses.h" />
<ClInclude Include="StdInc.h" />
<ClInclude Include="BattleAI.h" />
<ClInclude Include="..\..\Global.h" />
<ClInclude Include="ThreatMap.h" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

View File

@ -17,6 +17,7 @@
#include "../../lib/CStopWatch.h"
#include "../../lib/CThreadHelper.h"
#include "../../lib/mapObjects/CGTownInstance.h"
#include "../../lib/entities/building/TownFortifications.h"
#include "../../lib/spells/CSpellHandler.h"
#include "../../lib/spells/ISpellMechanics.h"
#include "../../lib/battle/BattleStateInfoForRetreat.h"
@ -49,6 +50,43 @@ SpellTypes spellType(const CSpell * spell)
return SpellTypes::OTHER;
}
BattleEvaluator::BattleEvaluator(
std::shared_ptr<Environment> env,
std::shared_ptr<CBattleCallback> cb,
const battle::Unit * activeStack,
PlayerColor playerID,
BattleID battleID,
BattleSide side,
float strengthRatio,
int simulationTurnsCount)
:scoreEvaluator(cb->getBattle(battleID), env, strengthRatio, simulationTurnsCount),
cachedAttack(), playerID(playerID), side(side), env(env),
cb(cb), strengthRatio(strengthRatio), battleID(battleID), simulationTurnsCount(simulationTurnsCount)
{
hb = std::make_shared<HypotheticBattle>(env.get(), cb->getBattle(battleID));
damageCache.buildDamageCache(hb, side);
targets = std::make_unique<PotentialTargets>(activeStack, damageCache, hb);
}
BattleEvaluator::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,
BattleSide side,
float strengthRatio,
int simulationTurnsCount)
:scoreEvaluator(cb->getBattle(battleID), env, strengthRatio, simulationTurnsCount),
cachedAttack(), playerID(playerID), side(side), env(env), cb(cb), hb(hb),
damageCache(damageCache), strengthRatio(strengthRatio), battleID(battleID), simulationTurnsCount(simulationTurnsCount)
{
targets = std::make_unique<PotentialTargets>(activeStack, damageCache, hb);
}
std::vector<BattleHex> BattleEvaluator::getBrokenWallMoatHexes() const
{
std::vector<BattleHex> result;
@ -81,6 +119,14 @@ std::vector<BattleHex> BattleEvaluator::getBrokenWallMoatHexes() const
return result;
}
bool BattleEvaluator::hasWorkingTowers() const
{
bool keepIntact = cb->getBattle(battleID)->battleGetWallState(EWallPart::KEEP) != EWallState::NONE && cb->getBattle(battleID)->battleGetWallState(EWallPart::KEEP) != EWallState::DESTROYED;
bool upperIntact = cb->getBattle(battleID)->battleGetWallState(EWallPart::UPPER_TOWER) != EWallState::NONE && cb->getBattle(battleID)->battleGetWallState(EWallPart::UPPER_TOWER) != EWallState::DESTROYED;
bool bottomIntact = cb->getBattle(battleID)->battleGetWallState(EWallPart::BOTTOM_TOWER) != EWallState::NONE && cb->getBattle(battleID)->battleGetWallState(EWallPart::BOTTOM_TOWER) != EWallState::DESTROYED;
return keepIntact || upperIntact || bottomIntact;
}
std::optional<PossibleSpellcast> BattleEvaluator::findBestCreatureSpell(const CStack *stack)
{
//TODO: faerie dragon type spell should be selected by server
@ -123,6 +169,14 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
auto moveTarget = scoreEvaluator.findMoveTowardsUnreachable(stack, *targets, damageCache, hb);
float score = EvaluationResult::INEFFECTIVE_SCORE;
auto enemyMellee = hb->getUnitsIf([this](const battle::Unit* u) -> bool
{
return u->unitSide() == BattleSide::ATTACKER && !hb->battleCanShoot(u);
});
bool siegeDefense = stack->unitSide() == BattleSide::DEFENDER
&& !stack->canShoot()
&& hasWorkingTowers()
&& !enemyMellee.empty();
if(targets->possibleAttacks.empty() && bestSpellcast.has_value())
{
@ -136,11 +190,13 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
logAi->trace("Evaluating attack for %s", stack->getDescription());
#endif
auto evaluationResult = scoreEvaluator.findBestTarget(stack, *targets, damageCache, hb);
auto evaluationResult = scoreEvaluator.findBestTarget(stack, *targets, damageCache, hb, siegeDefense);
auto & bestAttack = evaluationResult.bestAttack;
cachedAttack = bestAttack;
cachedScore = evaluationResult.score;
cachedAttack.ap = bestAttack;
cachedAttack.score = evaluationResult.score;
cachedAttack.turn = 0;
cachedAttack.waited = evaluationResult.wait;
//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())
@ -167,7 +223,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
score
);
if (moveTarget.scorePerTurn <= score)
if (moveTarget.score <= score)
{
if(evaluationResult.wait)
{
@ -186,37 +242,59 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
{
return BattleAction::makeDefend(stack);
}
else
bool isTargetOutsideFort = !hb->battleIsInsideWalls(bestAttack.from);
bool siegeDefense = stack->unitSide() == BattleSide::DEFENDER
&& !bestAttack.attack.shooting
&& hasWorkingTowers()
&& !enemyMellee.empty()
&& isTargetOutsideFort;
if(siegeDefense)
{
activeActionMade = true;
return BattleAction::makeMeleeAttack(stack, bestAttack.attack.defender->getPosition(), bestAttack.from);
logAi->trace("Evaluating exchange at %d self-defense", stack->getPosition().hex);
BattleAttackInfo bai(stack, stack, 0, false);
AttackPossibility apDefend(stack->getPosition(), stack->getPosition(), bai);
float defenseValue = scoreEvaluator.evaluateExchange(apDefend, 0, *targets, damageCache, hb);
if((defenseValue > score && score <= 0) || (defenseValue > 2 * score && score > 0))
{
return BattleAction::makeDefend(stack);
}
}
activeActionMade = true;
return BattleAction::makeMeleeAttack(stack, bestAttack.attack.defenderPos, bestAttack.from);
}
}
}
}
//ThreatMap threatsToUs(stack); // These lines may be usefull but they are't used in the code.
if(moveTarget.scorePerTurn > score)
//ThreatMap threatsToUs(stack); // These lines may be useful but they are't used in the code.
if(moveTarget.score > score)
{
score = moveTarget.score;
cachedAttack = moveTarget.cachedAttack;
cachedScore = score;
cachedAttack.ap = moveTarget.cachedAttack;
cachedAttack.score = score;
cachedAttack.turn = moveTarget.turnsToRich;
if(stack->waited())
{
logAi->debug(
"Moving %s towards hex %s[%d], score: %2f/%2f",
"Moving %s towards hex %s[%d], score: %2f",
stack->getDescription(),
moveTarget.cachedAttack->attack.defender->getDescription(),
moveTarget.cachedAttack->attack.defender->getPosition().hex,
moveTarget.score,
moveTarget.scorePerTurn);
moveTarget.score);
return goTowardsNearest(stack, moveTarget.positions);
return goTowardsNearest(stack, moveTarget.positions, *targets);
}
else
{
cachedAttack.waited = true;
return BattleAction::makeWait(stack);
}
}
@ -224,7 +302,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
if(score <= EvaluationResult::INEFFECTIVE_SCORE
&& !stack->hasBonusOfType(BonusType::FLYING)
&& stack->unitSide() == BattleSide::ATTACKER
&& cb->getBattle(battleID)->battleGetSiegeLevel() >= CGTownInstance::CITADEL)
&& cb->getBattle(battleID)->battleGetFortifications().hasMoat)
{
auto brokenWallMoat = getBrokenWallMoatHexes();
@ -235,7 +313,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
if(stack->doubleWide() && vstd::contains(brokenWallMoat, stack->getPosition()))
return BattleAction::makeMove(stack, stack->getPosition().cloneInDirection(BattleHex::RIGHT));
else
return goTowardsNearest(stack, brokenWallMoat);
return goTowardsNearest(stack, brokenWallMoat, *targets);
}
}
@ -249,11 +327,55 @@ uint64_t timeElapsed(std::chrono::time_point<std::chrono::high_resolution_clock>
return std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
}
BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes)
BattleAction BattleEvaluator::moveOrAttack(const CStack * stack, BattleHex hex, const PotentialTargets & targets)
{
auto additionalScore = 0;
std::optional<AttackPossibility> attackOnTheWay;
for(auto & target : targets.possibleAttacks)
{
if(!target.attack.shooting && target.from == hex && target.attackValue() > additionalScore)
{
additionalScore = target.attackValue();
attackOnTheWay = target;
}
}
if(attackOnTheWay)
{
activeActionMade = true;
return BattleAction::makeMeleeAttack(stack, attackOnTheWay->attack.defender->getPosition(), attackOnTheWay->from);
}
else
{
if(stack->position == hex)
return BattleAction::makeDefend(stack);
else
return BattleAction::makeMove(stack, hex);
}
}
BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes, const PotentialTargets & targets)
{
auto reachability = cb->getBattle(battleID)->getReachability(stack);
auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false);
auto enemyMellee = hb->getUnitsIf([this](const battle::Unit* u) -> bool
{
return u->unitSide() == BattleSide::ATTACKER && !hb->battleCanShoot(u);
});
bool siegeDefense = stack->unitSide() == BattleSide::DEFENDER
&& hasWorkingTowers()
&& !enemyMellee.empty();
if (siegeDefense)
{
vstd::erase_if(avHexes, [&](const BattleHex& hex) {
return !cb->getBattle(battleID)->battleIsInsideWalls(hex);
});
}
if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked
{
return BattleAction::makeDefend(stack);
@ -261,56 +383,45 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector
std::vector<BattleHex> targetHexes = hexes;
for(int i = 0; i < 5; i++)
{
std::sort(targetHexes.begin(), targetHexes.end(), [&](BattleHex h1, BattleHex h2) -> bool
{
return reachability.distances.at(h1) < reachability.distances.at(h2);
});
vstd::erase_if(targetHexes, [](const BattleHex & hex) { return !hex.isValid(); });
for(auto hex : targetHexes)
std::sort(targetHexes.begin(), targetHexes.end(), [&](BattleHex h1, BattleHex h2) -> bool
{
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);
}
}
if(reachability.distances.at(targetHexes.front()) <= GameConstants::BFIELD_SIZE)
{
break;
}
std::vector<BattleHex> copy = targetHexes;
for(auto hex : copy)
vstd::concatenate(targetHexes, hex.allNeighbouringTiles());
vstd::erase_if(targetHexes, [](const BattleHex & hex) {return !hex.isValid();});
vstd::removeDuplicates(targetHexes);
}
return reachability.distances[h1] < reachability.distances[h2];
});
BattleHex bestNeighbor = targetHexes.front();
if(reachability.distances.at(bestNeighbor) > GameConstants::BFIELD_SIZE)
if(reachability.distances[bestNeighbor] > GameConstants::BFIELD_SIZE)
{
logAi->trace("No richable hexes.");
return BattleAction::makeDefend(stack);
}
// this turn
for(auto hex : targetHexes)
{
if(vstd::contains(avHexes, hex))
{
return moveOrAttack(stack, hex, targets);
}
if(stack->coversPos(hex))
{
logAi->warn("Warning: already standing on neighbouring hex!");
//We shouldn't even be here...
return BattleAction::makeDefend(stack);
}
}
// not this turn
scoreEvaluator.updateReachabilityMap(hb);
if(stack->hasBonusOfType(BonusType::FLYING))
{
std::set<BattleHex> obstacleHexes;
auto insertAffected = [](const CObstacleInstance & spellObst, std::set<BattleHex> obstacleHexes) {
auto insertAffected = [](const CObstacleInstance & spellObst, std::set<BattleHex> & obstacleHexes) {
auto affectedHexes = spellObst.getAffectedTiles();
obstacleHexes.insert(affectedHexes.cbegin(), affectedHexes.cend());
};
@ -343,7 +454,7 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector
return scoreEvaluator.checkPositionBlocksOurStacks(*hb, stack, hex) ? BLOCKED_STACK_PENALTY + distance : distance;
});
return BattleAction::makeMove(stack, *nearestAvailableHex);
return moveOrAttack(stack, *nearestAvailableHex, targets);
}
else
{
@ -357,11 +468,16 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector
if(vstd::contains(avHexes, currentDest)
&& !scoreEvaluator.checkPositionBlocksOurStacks(*hb, stack, currentDest))
return BattleAction::makeMove(stack, currentDest);
{
return moveOrAttack(stack, currentDest, targets);
}
currentDest = reachability.predecessors[currentDest];
}
}
logAi->error("We should either detect that hexes are unreachable or make a move!");
return BattleAction::makeDefend(stack);
}
bool BattleEvaluator::canCastSpell()
@ -391,7 +507,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
vstd::erase_if(possibleSpells, [](const CSpell *s)
{
return spellType(s) != SpellTypes::BATTLE || s->getTargetType() == spells::AimType::LOCATION;
return spellType(s) != SpellTypes::BATTLE;
});
LOGFL("I know how %d of them works.", possibleSpells.size());
@ -402,9 +518,6 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
{
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))
@ -573,7 +686,15 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
auto & ps = possibleCasts[i];
#if BATTLE_TRACE_LEVEL >= 1
logAi->trace("Evaluating %s", ps.spell->getNameTranslated());
if(ps.dest.empty())
logAi->trace("Evaluating %s", ps.spell->getNameTranslated());
else
{
auto psFirst = ps.dest.front();
auto strWhere = psFirst.unitValue ? psFirst.unitValue->getDescription() : std::to_string(psFirst.hexValue.hex);
logAi->trace("Evaluating %s at %s", ps.spell->getNameTranslated(), strWhere);
}
#endif
auto state = std::make_shared<HypotheticBattle>(env.get(), cb->getBattle(battleID));
@ -581,7 +702,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
spells::BattleCast cast(state.get(), hero, spells::Mode::HERO, ps.spell);
cast.castEval(state->getServerCallback(), ps.dest);
auto allUnits = state->battleGetUnitsIf([](const battle::Unit * u) -> bool { return true; });
auto allUnits = state->battleGetUnitsIf([](const battle::Unit * u) -> bool { return u->isValidTarget(); });
auto needFullEval = vstd::contains_if(allUnits, [&](const battle::Unit * u) -> bool
{
@ -591,40 +712,57 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
DamageCache safeCopy = damageCache;
DamageCache innerCache(&safeCopy);
innerCache.buildDamageCache(state, side);
if(needFullEval || !cachedAttack)
if(cachedAttack.ap && cachedAttack.waited)
{
state->makeWait(activeStack);
}
if(needFullEval || !cachedAttack.ap)
{
#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);
BattleExchangeEvaluator innerEvaluator(state, env, strengthRatio, simulationTurnsCount);
innerEvaluator.updateReachabilityMap(state);
auto moveTarget = innerEvaluator.findMoveTowardsUnreachable(activeStack, innerTargets, innerCache, state);
if(!innerTargets.possibleAttacks.empty())
{
innerEvaluator.updateReachabilityMap(state);
auto newStackAction = innerEvaluator.findBestTarget(activeStack, innerTargets, innerCache, state);
ps.value = newStackAction.score;
ps.value = std::max(moveTarget.score, newStackAction.score);
}
else
{
ps.value = 0;
ps.value = moveTarget.score;
}
}
else
{
ps.value = scoreEvaluator.evaluateExchange(*cachedAttack, 0, *targets, innerCache, state);
}
auto updatedAttacker = state->getForUpdate(cachedAttack.ap->attack.attacker->unitId());
auto updatedDefender = state->getForUpdate(cachedAttack.ap->attack.defender->unitId());
auto updatedBai = BattleAttackInfo(
updatedAttacker.get(),
updatedDefender.get(),
cachedAttack.ap->attack.chargeDistance,
cachedAttack.ap->attack.shooting);
auto updatedAttack = AttackPossibility::evaluate(updatedBai, cachedAttack.ap->from, innerCache, state);
ps.value = scoreEvaluator.evaluateExchange(updatedAttack, cachedAttack.turn, *targets, innerCache, state);
}
for(const auto & unit : allUnits)
{
if (!unit->isValidTarget())
if(!unit->isValidTarget(true))
continue;
auto newHealth = unit->getAvailableHealth();
auto oldHealth = vstd::find_or(healthOfStack, unit->unitId(), 0); // old health value may not exist for newly summoned units
@ -635,7 +773,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
auto dpsReduce = AttackPossibility::calculateDamageReduce(
nullptr,
originalDefender && originalDefender->alive() ? originalDefender : unit,
originalDefender && originalDefender->alive() ? originalDefender : unit,
damage,
innerCache,
state);
@ -645,23 +783,49 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
if(ourUnit * goodEffect == 1)
{
if(ourUnit && goodEffect && (unit->isClone() || unit->isGhost()))
auto isMagical = state->getForUpdate(unit->unitId())->summoned
|| unit->isClone()
|| unit->isGhost();
if(ourUnit && goodEffect && isMagical)
continue;
ps.value += dpsReduce * scoreEvaluator.getPositiveEffectMultiplier();
}
else
ps.value -= dpsReduce * scoreEvaluator.getNegativeEffectMultiplier();
// discourage AI making collateral damage with spells
ps.value -= 4 * dpsReduce * scoreEvaluator.getNegativeEffectMultiplier();
#if BATTLE_TRACE_LEVEL >= 1
logAi->trace(
"Spell affects %s (%d), dps: %2f",
unit->creatureId().toCreature()->getNameSingularTranslated(),
unit->getCount(),
dpsReduce);
// Ensure ps.dest is not empty before accessing the first element
if (!ps.dest.empty())
{
logAi->trace(
"Spell %s to %d affects %s (%d), dps: %2f oldHealth: %d newHealth: %d",
ps.spell->getNameTranslated(),
ps.dest.at(0).hexValue.hex, // Safe to access .at(0) now
unit->creatureId().toCreature()->getNameSingularTranslated(),
unit->getCount(),
dpsReduce,
oldHealth,
newHealth);
}
else
{
// Handle the case where ps.dest is empty
logAi->trace(
"Spell %s has no destination, affects %s (%d), dps: %2f oldHealth: %d newHealth: %d",
ps.spell->getNameTranslated(),
unit->creatureId().toCreature()->getNameSingularTranslated(),
unit->getCount(),
dpsReduce,
oldHealth,
newHealth);
}
#endif
}
}
#if BATTLE_TRACE_LEVEL >= 1
logAi->trace("Total score: %2f", ps.value);
#endif
@ -672,13 +836,12 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
LOGFL("Evaluation took %d ms", timer.getDiff());
auto pscValue = [](const PossibleSpellcast &ps) -> float
{
return ps.value;
};
auto castToPerform = *vstd::maxElementByFun(possibleCasts, pscValue);
auto castToPerform = *vstd::maxElementByFun(possibleCasts, [](const PossibleSpellcast & ps) -> float
{
return ps.value;
});
if(castToPerform.value > cachedScore)
if(castToPerform.value > cachedAttack.score && !vstd::isAlmostEqual(castToPerform.value, cachedAttack.score))
{
LOGFL("Best spell is %s (value %d). Will cast.", castToPerform.spell->getNameTranslated() % castToPerform.value);
BattleAction spellcast;
@ -686,7 +849,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
spellcast.spell = castToPerform.spell->id;
spellcast.setTarget(castToPerform.dest);
spellcast.side = side;
spellcast.stackNumber = (!side) ? -1 : -2;
spellcast.stackNumber = -1;
cb->battleMakeSpellAction(battleID, spellcast);
activeActionMade = true;

View File

@ -22,6 +22,14 @@ VCMI_LIB_NAMESPACE_END
class EnemyInfo;
struct CachedAttack
{
std::optional<AttackPossibility> ap;
float score = EvaluationResult::INEFFECTIVE_SCORE;
uint8_t turn = 255;
bool waited = false;
};
class BattleEvaluator
{
std::unique_ptr<PotentialTargets> targets;
@ -30,23 +38,25 @@ class BattleEvaluator
std::shared_ptr<CBattleCallback> cb;
std::shared_ptr<Environment> env;
bool activeActionMade = false;
std::optional<AttackPossibility> cachedAttack;
CachedAttack cachedAttack;
PlayerColor playerID;
BattleID battleID;
int side;
float cachedScore;
BattleSide side;
DamageCache damageCache;
float strengthRatio;
int simulationTurnsCount;
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);
BattleAction goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes, const PotentialTargets & targets);
std::vector<BattleHex> getBrokenWallMoatHexes() const;
bool hasWorkingTowers() const;
void evaluateCreatureSpellcast(const CStack * stack, PossibleSpellcast & ps); //for offensive damaging spells only
void print(const std::string & text) const;
BattleAction moveOrAttack(const CStack * stack, BattleHex hex, const PotentialTargets & targets);
BattleEvaluator(
std::shared_ptr<Environment> env,
@ -54,16 +64,9 @@ public:
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;
}
BattleSide side,
float strengthRatio,
int simulationTurnsCount);
BattleEvaluator(
std::shared_ptr<Environment> env,
@ -73,11 +76,7 @@ public:
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;
}
BattleSide side,
float strengthRatio,
int simulationTurnsCount);
};

View File

@ -9,16 +9,17 @@
*/
#include "StdInc.h"
#include "BattleExchangeVariant.h"
#include "BattleEvaluator.h"
#include "../../lib/CStack.h"
AttackerValue::AttackerValue()
: value(0),
isRetalitated(false)
isRetaliated(false)
{
}
MoveTarget::MoveTarget()
: positions(), cachedAttack(), score(EvaluationResult::INEFFECTIVE_SCORE), scorePerTurn(EvaluationResult::INEFFECTIVE_SCORE)
: positions(), cachedAttack(), score(EvaluationResult::INEFFECTIVE_SCORE)
{
turnsToRich = 1;
}
@ -28,102 +29,97 @@ float BattleExchangeVariant::trackAttack(
std::shared_ptr<HypotheticBattle> hb,
DamageCache & damageCache)
{
if(!ap.attackerState)
{
logAi->trace("Skipping fake ap attack");
return 0;
}
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;
float attackValue = ap.attackValue();
auto affectedUnits = ap.affectedUnits;
dpsScore.ourDamageReduce += ap.attackerDamageReduce + ap.collateralDamageReduce;
dpsScore.enemyDamageReduce += ap.defenderDamageReduce + ap.shootersBlockedDmg;
attackerValue[attacker->unitId()].value = attackValue;
affectedUnits.push_back(ap.attackerState);
for(auto affectedUnit : affectedUnits)
{
auto unitToUpdate = hb->getForUpdate(affectedUnit->unitId());
auto damageDealt = unitToUpdate->getAvailableHealth() - affectedUnit->getAvailableHealth();
if(damageDealt > 0)
{
unitToUpdate->damage(damageDealt);
}
if(unitToUpdate->unitSide() == attacker->unitSide())
{
if(unitToUpdate->unitId() == attacker->unitId())
{
auto defender = hb->getForUpdate(ap.attack.defender->unitId());
if(!defender->alive() || counterAttacksBlocked || ap.attack.shooting || !defender->ableToRetaliate())
continue;
auto retaliationDamage = damageCache.getDamage(defender.get(), unitToUpdate.get(), hb);
auto attackerDamageReduce = AttackPossibility::calculateDamageReduce(defender.get(), unitToUpdate.get(), retaliationDamage, damageCache, hb);
attackValue -= attackerDamageReduce;
dpsScore.ourDamageReduce += attackerDamageReduce;
attackerValue[unitToUpdate->unitId()].isRetalitated = true;
unitToUpdate->damage(retaliationDamage);
defender->afterAttack(false, true);
unitToUpdate->afterAttack(ap.attack.shooting, false);
#if BATTLE_TRACE_LEVEL>=1
logAi->trace(
"%s -> %s, ap retalitation, %s, dps: %2f, score: %2f",
defender->getDescription(),
unitToUpdate->getDescription(),
"%s -> %s, ap retaliation, %s, dps: %lld",
hb->getForUpdate(ap.attack.defender->unitId())->getDescription(),
ap.attack.attacker->getDescription(),
ap.attack.shooting ? "shot" : "mellee",
retaliationDamage,
attackerDamageReduce);
damageDealt);
#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.ourDamageReduce += collateralDamageReduce;
unitToUpdate->damage(collateralDamage);
#if BATTLE_TRACE_LEVEL>=1
logAi->trace(
"%s -> %s, ap collateral, %s, dps: %2f, score: %2f",
attacker->getDescription(),
"%s, ap collateral, dps: %lld",
unitToUpdate->getDescription(),
ap.attack.shooting ? "shot" : "mellee",
collateralDamage,
collateralDamageReduce);
damageDealt);
#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.enemyDamageReduce += defenderDamageReduce;
attackerValue[attacker->unitId()].value += defenderDamageReduce;
unitToUpdate->damage(attackDamage);
if(unitToUpdate->unitId() == ap.attack.defender->unitId())
{
if(unitToUpdate->ableToRetaliate() && !affectedUnit->ableToRetaliate())
{
unitToUpdate->afterAttack(ap.attack.shooting, true);
}
#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);
logAi->trace(
"%s -> %s, ap attack, %s, dps: %lld",
attacker->getDescription(),
ap.attack.defender->getDescription(),
ap.attack.shooting ? "shot" : "mellee",
damageDealt);
#endif
}
else
{
#if BATTLE_TRACE_LEVEL>=1
logAi->trace(
"%s, ap enemy collateral, dps: %lld",
unitToUpdate->getDescription(),
damageDealt);
#endif
}
}
}
#if BATTLE_TRACE_LEVEL >= 1
logAi->trace("ap shooters blocking: %lld", ap.shootersBlockedDmg);
logAi->trace(
"ap score: our: %2f, enemy: %2f, collateral: %2f, blocked: %2f",
ap.attackerDamageReduce,
ap.defenderDamageReduce,
ap.collateralDamageReduce,
ap.shootersBlockedDmg);
#endif
attackValue += ap.shootersBlockedDmg;
dpsScore.enemyDamageReduce += ap.shootersBlockedDmg;
attacker->afterAttack(ap.attack.shooting, false);
return attackValue;
}
@ -185,7 +181,7 @@ float BattleExchangeVariant::trackAttack(
if(isOurAttack)
{
dpsScore.ourDamageReduce += attackerDamageReduce;
attackerValue[attacker->unitId()].isRetalitated = true;
attackerValue[attacker->unitId()].isRetaliated = true;
}
else
{
@ -218,7 +214,8 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
const battle::Unit * activeStack,
PotentialTargets & targets,
DamageCache & damageCache,
std::shared_ptr<HypotheticBattle> hb)
std::shared_ptr<HypotheticBattle> hb,
bool siegeDefense)
{
EvaluationResult result(targets.bestAction());
@ -230,13 +227,15 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
auto hbWaited = std::make_shared<HypotheticBattle>(env.get(), hb);
hbWaited->getForUpdate(activeStack->unitId())->waiting = true;
hbWaited->getForUpdate(activeStack->unitId())->waitedThisTurn = true;
hbWaited->makeWait(activeStack);
updateReachabilityMap(hbWaited);
for(auto & ap : targets.possibleAttacks)
{
if (siegeDefense && !hb->battleIsInsideWalls(ap.from))
continue;
float score = evaluateExchange(ap, 0, targets, damageCache, hbWaited);
if(score > result.score)
@ -259,6 +258,7 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
updateReachabilityMap(hb);
if(result.bestAttack.attack.shooting
&& !result.bestAttack.defenderDead
&& !activeStack->waited()
&& hb->battleHasShootingPenalty(activeStack, result.bestAttack.dest))
{
@ -268,9 +268,13 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
for(auto & ap : targets.possibleAttacks)
{
float score = evaluateExchange(ap, 0, targets, damageCache, hb);
if (siegeDefense && !hb->battleIsInsideWalls(ap.from))
continue;
if(score > result.score || (vstd::isAlmostEqual(score, result.score) && result.wait))
float score = evaluateExchange(ap, 0, targets, damageCache, hb);
bool sameScoreButWaited = vstd::isAlmostEqual(score, result.score) && result.wait;
if(score > result.score || sameScoreButWaited)
{
result.score = score;
result.bestAttack = ap;
@ -285,6 +289,36 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
return result;
}
ReachabilityInfo getReachabilityWithEnemyBypass(
const battle::Unit * activeStack,
DamageCache & damageCache,
std::shared_ptr<HypotheticBattle> state)
{
ReachabilityInfo::Parameters params(activeStack, activeStack->getPosition());
if(!params.flying)
{
for(const auto * unit : state->battleAliveUnits())
{
if(unit->unitSide() == activeStack->unitSide())
continue;
auto dmg = damageCache.getOriginalDamage(activeStack, unit, state);
auto turnsToKill = unit->getAvailableHealth() / std::max(dmg, (int64_t)1);
vstd::amin(turnsToKill, 100);
for(auto & hex : unit->getHexes())
if(hex.isAvailable()) //towers can have <0 pos; we don't also want to overwrite side columns
params.destructibleEnemyTurns[hex] = turnsToKill * unit->getMovementRange();
}
params.bypassEnemyStacks = true;
}
return state->getReachability(params);
}
MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
const battle::Unit * activeStack,
PotentialTargets & targets,
@ -294,6 +328,8 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
MoveTarget result;
BattleExchangeVariant ev;
logAi->trace("Find move towards unreachable. Enemies count %d", targets.unreachableEnemies.size());
if(targets.unreachableEnemies.empty())
return result;
@ -304,17 +340,17 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
updateReachabilityMap(hb);
auto dists = cb->getReachability(activeStack);
auto dists = getReachabilityWithEnemyBypass(activeStack, damageCache, hb);
auto flying = activeStack->hasBonusOfType(BonusType::FLYING);
for(const battle::Unit * enemy : targets.unreachableEnemies)
{
std::vector<const battle::Unit *> adjacentStacks = getAdjacentUnits(enemy);
auto closestStack = *vstd::minElementByFun(adjacentStacks, [&](const battle::Unit * u) -> int64_t
{
return dists.distToNearestNeighbour(activeStack, u) * 100000 - activeStack->getTotalHealth();
});
logAi->trace(
"Checking movement towards %d of %s",
enemy->getCount(),
enemy->creatureId().toCreature()->getNameSingularTranslated());
auto distance = dists.distToNearestNeighbour(activeStack, closestStack);
auto distance = dists.distToNearestNeighbour(activeStack, enemy);
if(distance >= GameConstants::BFIELD_SIZE)
continue;
@ -322,31 +358,109 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
if(distance <= speed)
continue;
float penaltyMultiplier = 1.0f; // Default multiplier, no penalty
float closestAllyDistance = std::numeric_limits<float>::max();
for (const battle::Unit* ally : hb->battleAliveUnits()) {
if (ally == activeStack)
continue;
if (ally->unitSide() != activeStack->unitSide())
continue;
float allyDistance = dists.distToNearestNeighbour(ally, enemy);
if (allyDistance < closestAllyDistance)
{
closestAllyDistance = allyDistance;
}
}
// If an ally is closer to the enemy, compute the penaltyMultiplier
if (closestAllyDistance < distance) {
penaltyMultiplier = closestAllyDistance / distance; // Ratio of distances
}
auto turnsToRich = (distance - 1) / speed + 1;
auto hexes = closestStack->getSurroundingHexes();
auto enemySpeed = closestStack->getMovementRange();
auto hexes = enemy->getSurroundingHexes();
auto enemySpeed = enemy->getMovementRange();
auto speedRatio = speed / static_cast<float>(enemySpeed);
auto multiplier = speedRatio > 1 ? 1 : speedRatio;
auto multiplier = (speedRatio > 1 ? 1 : speedRatio) * penaltyMultiplier;
if(enemy->canShoot())
multiplier *= 1.5f;
for(auto hex : hexes)
for(auto & hex : hexes)
{
// FIXME: provide distance info for Jousting bonus
auto bai = BattleAttackInfo(activeStack, closestStack, 0, cb->battleCanShoot(activeStack));
auto bai = BattleAttackInfo(activeStack, enemy, 0, cb->battleCanShoot(activeStack));
auto attack = AttackPossibility::evaluate(bai, hex, damageCache, hb);
attack.shootersBlockedDmg = 0; // we do not want to count on it, it is not for sure
auto score = calculateExchange(attack, turnsToRich, targets, damageCache, hb);
auto scorePerTurn = BattleScore(score.enemyDamageReduce * std::sqrt(multiplier / turnsToRich), score.ourDamageReduce);
if(result.scorePerTurn < scoreValue(scorePerTurn))
score.enemyDamageReduce *= multiplier;
#if BATTLE_TRACE_LEVEL >= 1
logAi->trace("Multiplier: %f, turns: %d, current score %f, new score %f", multiplier, turnsToRich, result.score, scoreValue(score));
#endif
if(result.score < scoreValue(score)
|| (result.turnsToRich > turnsToRich && vstd::isAlmostEqual(result.score, scoreValue(score))))
{
result.scorePerTurn = scoreValue(scorePerTurn);
result.score = scoreValue(score);
result.positions = closestStack->getAttackableHexes(activeStack);
result.positions.clear();
#if BATTLE_TRACE_LEVEL >= 1
logAi->trace("New high score");
#endif
for(const BattleHex & initialEnemyHex : enemy->getAttackableHexes(activeStack))
{
BattleHex enemyHex = initialEnemyHex;
while(!flying && dists.distances[enemyHex] > speed && dists.predecessors.at(enemyHex).isValid())
{
enemyHex = dists.predecessors.at(enemyHex);
if(dists.accessibility[enemyHex] == EAccessibility::ALIVE_STACK)
{
auto defenderToBypass = hb->battleGetUnitByPos(enemyHex);
if(defenderToBypass)
{
#if BATTLE_TRACE_LEVEL >= 1
logAi->trace("Found target to bypass at %d", enemyHex.hex);
#endif
auto attackHex = dists.predecessors[enemyHex];
auto baiBypass = BattleAttackInfo(activeStack, defenderToBypass, 0, cb->battleCanShoot(activeStack));
auto attackBypass = AttackPossibility::evaluate(baiBypass, attackHex, damageCache, hb);
auto adjacentStacks = getAdjacentUnits(enemy);
adjacentStacks.push_back(defenderToBypass);
vstd::removeDuplicates(adjacentStacks);
auto bypassScore = calculateExchange(
attackBypass,
dists.distances[attackHex],
targets,
damageCache,
hb,
adjacentStacks);
if(scoreValue(bypassScore) > result.score)
{
result.score = scoreValue(bypassScore);
#if BATTLE_TRACE_LEVEL >= 1
logAi->trace("New high score after bypass %f", scoreValue(bypassScore));
#endif
}
}
}
}
result.positions.push_back(enemyHex);
}
result.cachedAttack = attack;
result.turnsToRich = turnsToRich;
}
@ -390,7 +504,8 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits(
const AttackPossibility & ap,
uint8_t turn,
PotentialTargets & targets,
std::shared_ptr<HypotheticBattle> hb) const
std::shared_ptr<HypotheticBattle> hb,
std::vector<const battle::Unit *> additionalUnits) const
{
ReachabilityData result;
@ -398,13 +513,29 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits(
if(!ap.attack.shooting) hexes.push_back(ap.from);
std::vector<const battle::Unit *> allReachableUnits;
std::vector<const battle::Unit *> allReachableUnits = additionalUnits;
for(auto hex : hexes)
{
vstd::concatenate(allReachableUnits, turn == 0 ? reachabilityMap.at(hex) : getOneTurnReachableUnits(turn, hex));
}
if(!ap.attack.attacker->isTurret())
{
for(auto hex : ap.attack.attacker->getHexes())
{
auto unitsReachingAttacker = turn == 0 ? reachabilityMap.at(hex) : getOneTurnReachableUnits(turn, hex);
for(auto unit : unitsReachingAttacker)
{
if(unit->unitSide() != ap.attack.attacker->unitSide())
{
allReachableUnits.push_back(unit);
result.enemyUnitsReachingAttacker.insert(unit->unitId());
}
}
}
}
vstd::removeDuplicates(allReachableUnits);
auto copy = allReachableUnits;
@ -440,7 +571,7 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits(
for(auto unit : allReachableUnits)
{
auto accessible = !unit->canShoot();
auto accessible = !unit->canShoot() || vstd::contains(additionalUnits, unit);
if(!accessible)
{
@ -464,14 +595,14 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits(
for(auto unit : turnOrder[turn])
{
if(vstd::contains(allReachableUnits, unit))
result.units.push_back(unit);
result.units[turn].push_back(unit);
}
}
vstd::erase_if(result.units, [&](const battle::Unit * u) -> bool
{
return !hb->battleGetUnitByID(u->unitId())->alive();
});
vstd::erase_if(result.units[turn], [&](const battle::Unit * u) -> bool
{
return !hb->battleGetUnitByID(u->unitId())->alive();
});
}
return result;
}
@ -502,13 +633,14 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
uint8_t turn,
PotentialTargets & targets,
DamageCache & damageCache,
std::shared_ptr<HypotheticBattle> hb) const
std::shared_ptr<HypotheticBattle> hb,
std::vector<const battle::Unit *> additionalUnits) const
{
#if BATTLE_TRACE_LEVEL>=1
logAi->trace("Battle exchange at %d", ap.attack.shooting ? ap.dest.hex : ap.from.hex);
#endif
if(cb->battleGetMySide() == BattlePerspective::LEFT_SIDE
if(cb->battleGetMySide() == BattleSide::LEFT_SIDE
&& cb->battleGetGateState() == EGateState::BLOCKED
&& ap.attack.defender->coversPos(BattleHex::GATE_BRIDGE))
{
@ -521,7 +653,7 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
if(hb->battleGetUnitByID(ap.attack.defender->unitId())->alive())
enemyStacks.push_back(ap.attack.defender);
ReachabilityData exchangeUnits = getExchangeUnits(ap, turn, targets, hb);
ReachabilityData exchangeUnits = getExchangeUnits(ap, turn, targets, hb, additionalUnits);
if(exchangeUnits.units.empty())
{
@ -531,22 +663,25 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
auto exchangeBattle = std::make_shared<HypotheticBattle>(env.get(), hb);
BattleExchangeVariant v;
for(auto unit : exchangeUnits.units)
for(int exchangeTurn = 0; exchangeTurn < exchangeUnits.units.size(); exchangeTurn++)
{
if(unit->isTurret())
continue;
bool isOur = exchangeBattle->battleMatchOwner(ap.attack.attacker, unit, true);
auto & attackerQueue = isOur ? ourStacks : enemyStacks;
auto u = exchangeBattle->getForUpdate(unit->unitId());
if(u->alive() && !vstd::contains(attackerQueue, unit))
for(auto unit : exchangeUnits.units.at(exchangeTurn))
{
attackerQueue.push_back(unit);
if(unit->isTurret())
continue;
bool isOur = exchangeBattle->battleMatchOwner(ap.attack.attacker, unit, true);
auto & attackerQueue = isOur ? ourStacks : enemyStacks;
auto u = exchangeBattle->getForUpdate(unit->unitId());
if(u->alive() && !vstd::contains(attackerQueue, unit))
{
attackerQueue.push_back(unit);
#if BATTLE_TRACE_LEVEL
logAi->trace("Exchanging: %s", u->getDescription());
logAi->trace("Exchanging: %s", u->getDescription());
#endif
}
}
}
@ -560,122 +695,166 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
bool canUseAp = true;
for(auto activeUnit : exchangeUnits.units)
std::set<uint32_t> blockedShooters;
int totalTurnsCount = simulationTurnsCount >= turn + turnOrder.size()
? simulationTurnsCount
: turn + turnOrder.size();
for(int exchangeTurn = 0; exchangeTurn < simulationTurnsCount; exchangeTurn++)
{
bool isOur = exchangeBattle->battleMatchOwner(ap.attack.attacker, activeUnit, true);
battle::Units & attackerQueue = isOur ? ourStacks : enemyStacks;
battle::Units & oppositeQueue = isOur ? enemyStacks : ourStacks;
bool isMovingTurm = exchangeTurn < turn;
int queueTurn = exchangeTurn >= exchangeUnits.units.size()
? exchangeUnits.units.size() - 1
: exchangeTurn;
auto attacker = exchangeBattle->getForUpdate(activeUnit->unitId());
if(!attacker->alive())
for(auto activeUnit : exchangeUnits.units.at(queueTurn))
{
bool isOur = exchangeBattle->battleMatchOwner(ap.attack.attacker, activeUnit, true);
battle::Units & attackerQueue = isOur ? ourStacks : enemyStacks;
battle::Units & oppositeQueue = isOur ? enemyStacks : ourStacks;
auto attacker = exchangeBattle->getForUpdate(activeUnit->unitId());
auto shooting = exchangeBattle->battleCanShoot(attacker.get())
&& !vstd::contains(blockedShooters, attacker->unitId());
if(!attacker->alive())
{
#if BATTLE_TRACE_LEVEL>=1
logAi->trace( "Attacker is dead");
logAi->trace("Attacker is dead");
#endif
continue;
}
auto targetUnit = ap.attack.defender;
if(!isOur || !exchangeBattle->battleGetUnitByID(targetUnit->unitId())->alive())
{
auto estimateAttack = [&](const battle::Unit * u) -> float
{
auto stackWithBonuses = exchangeBattle->getForUpdate(u->unitId());
auto score = v.trackAttack(
attacker,
stackWithBonuses,
exchangeBattle->battleCanShoot(stackWithBonuses.get()),
isOur,
damageCache,
hb,
true);
#if BATTLE_TRACE_LEVEL>=1
logAi->trace("Best target selector %s->%s score = %2f", attacker->getDescription(), stackWithBonuses->getDescription(), score);
#endif
return score;
};
auto unitsInOppositeQueueExceptInaccessible = oppositeQueue;
vstd::erase_if(unitsInOppositeQueueExceptInaccessible, [&](const battle::Unit * u)->bool
{
return vstd::contains(exchangeUnits.shooters, u);
});
if(!unitsInOppositeQueueExceptInaccessible.empty())
{
targetUnit = *vstd::maxElementByFun(unitsInOppositeQueueExceptInaccessible, estimateAttack);
continue;
}
else
if(isMovingTurm && !shooting
&& !vstd::contains(exchangeUnits.enemyUnitsReachingAttacker, attacker->unitId()))
{
auto reachable = exchangeBattle->battleGetUnitsIf([this, &exchangeBattle, &attacker](const battle::Unit * u) -> bool
#if BATTLE_TRACE_LEVEL>=1
logAi->trace("Attacker is moving");
#endif
continue;
}
auto targetUnit = ap.attack.defender;
if(!isOur || !exchangeBattle->battleGetUnitByID(targetUnit->unitId())->alive())
{
#if BATTLE_TRACE_LEVEL>=2
logAi->trace("Best target selector for %s", attacker->getDescription());
#endif
auto estimateAttack = [&](const battle::Unit * u) -> float
{
auto stackWithBonuses = exchangeBattle->getForUpdate(u->unitId());
auto score = v.trackAttack(
attacker,
stackWithBonuses,
exchangeBattle->battleCanShoot(stackWithBonuses.get()),
isOur,
damageCache,
hb,
true);
#if BATTLE_TRACE_LEVEL>=2
logAi->trace("Best target selector %s->%s score = %2f", attacker->getDescription(), stackWithBonuses->getDescription(), score);
#endif
return score;
};
auto unitsInOppositeQueueExceptInaccessible = oppositeQueue;
vstd::erase_if(unitsInOppositeQueueExceptInaccessible, [&](const battle::Unit * u)->bool
{
if(u->unitSide() == attacker->unitSide())
return false;
if(!exchangeBattle->getForUpdate(u->unitId())->alive())
return false;
if (!u->getPosition().isValid())
return false; // e.g. tower shooters
return vstd::contains_if(reachabilityMap.at(u->getPosition()), [&attacker](const battle::Unit * other) -> bool
{
return attacker->unitId() == other->unitId();
});
return vstd::contains(exchangeUnits.shooters, u);
});
if(!reachable.empty())
if(!isOur
&& exchangeTurn == 0
&& exchangeUnits.units.at(exchangeTurn).at(0)->unitId() != ap.attack.attacker->unitId()
&& !vstd::contains(exchangeUnits.enemyUnitsReachingAttacker, attacker->unitId()))
{
targetUnit = *vstd::maxElementByFun(reachable, estimateAttack);
vstd::erase_if(unitsInOppositeQueueExceptInaccessible, [&](const battle::Unit * u) -> bool
{
return u->unitId() == ap.attack.attacker->unitId();
});
}
if(!unitsInOppositeQueueExceptInaccessible.empty())
{
targetUnit = *vstd::maxElementByFun(unitsInOppositeQueueExceptInaccessible, estimateAttack);
}
else
{
auto reachable = exchangeBattle->battleGetUnitsIf([this, &exchangeBattle, &attacker](const battle::Unit * u) -> bool
{
if(u->unitSide() == attacker->unitSide())
return false;
if(!exchangeBattle->getForUpdate(u->unitId())->alive())
return false;
if(!u->getPosition().isValid())
return false; // e.g. tower shooters
return vstd::contains_if(reachabilityMap.at(u->getPosition()), [&attacker](const battle::Unit * other) -> bool
{
return attacker->unitId() == other->unitId();
});
});
if(!reachable.empty())
{
targetUnit = *vstd::maxElementByFun(reachable, estimateAttack);
}
else
{
#if BATTLE_TRACE_LEVEL>=1
logAi->trace("Battle queue is empty and no reachable enemy.");
logAi->trace("Battle queue is empty and no reachable enemy.");
#endif
continue;
continue;
}
}
}
}
auto defender = exchangeBattle->getForUpdate(targetUnit->unitId());
auto shooting = exchangeBattle->battleCanShoot(attacker.get());
const int totalAttacks = attacker->getTotalAttacks(shooting);
auto defender = exchangeBattle->getForUpdate(targetUnit->unitId());
const int totalAttacks = attacker->getTotalAttacks(shooting);
if(canUseAp && activeUnit->unitId() == ap.attack.attacker->unitId()
&& targetUnit->unitId() == ap.attack.defender->unitId())
{
v.trackAttack(ap, exchangeBattle, damageCache);
}
else
{
for(int i = 0; i < totalAttacks; i++)
if(canUseAp && activeUnit->unitId() == ap.attack.attacker->unitId()
&& targetUnit->unitId() == ap.attack.defender->unitId())
{
v.trackAttack(attacker, defender, shooting, isOur, damageCache, exchangeBattle);
if(!attacker->alive() || !defender->alive())
break;
v.trackAttack(ap, exchangeBattle, damageCache);
}
else
{
for(int i = 0; i < totalAttacks; i++)
{
v.trackAttack(attacker, defender, shooting, isOur, damageCache, exchangeBattle);
if(!attacker->alive() || !defender->alive())
break;
}
}
if(!shooting)
blockedShooters.insert(defender->unitId());
canUseAp = false;
vstd::erase_if(attackerQueue, [&](const battle::Unit * u) -> bool
{
return !exchangeBattle->battleGetUnitByID(u->unitId())->alive();
});
vstd::erase_if(oppositeQueue, [&](const battle::Unit * u) -> bool
{
return !exchangeBattle->battleGetUnitByID(u->unitId())->alive();
});
}
canUseAp = false;
vstd::erase_if(attackerQueue, [&](const battle::Unit * u) -> bool
{
return !exchangeBattle->battleGetUnitByID(u->unitId())->alive();
});
vstd::erase_if(oppositeQueue, [&](const battle::Unit * u) -> bool
{
return !exchangeBattle->battleGetUnitByID(u->unitId())->alive();
});
exchangeBattle->nextRound();
}
// avoid blocking path for stronger stack by weaker stack
@ -687,11 +866,28 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
for(auto hex : hexes)
reachabilityMap[hex] = getOneTurnReachableUnits(turn, hex);
auto score = v.getScore();
if(simulationTurnsCount < totalTurnsCount)
{
float scalingRatio = simulationTurnsCount / static_cast<float>(totalTurnsCount);
score.enemyDamageReduce *= scalingRatio;
score.ourDamageReduce *= scalingRatio;
}
if(turn > 0)
{
auto turnMultiplier = 1 - std::min(0.2, 0.05 * turn);
score.enemyDamageReduce *= turnMultiplier;
}
#if BATTLE_TRACE_LEVEL>=1
logAi->trace("Exchange score: enemy: %2f, our -%2f", v.getScore().enemyDamageReduce, v.getScore().ourDamageReduce);
logAi->trace("Exchange score: enemy: %2f, our -%2f", score.enemyDamageReduce, score.ourDamageReduce);
#endif
return v.getScore();
return score;
}
bool BattleExchangeEvaluator::canBeHitThisTurn(const AttackPossibility & ap)

View File

@ -45,7 +45,7 @@ struct BattleScore
struct AttackerValue
{
float value;
bool isRetalitated;
bool isRetaliated;
BattleHex position;
AttackerValue();
@ -54,7 +54,6 @@ struct AttackerValue
struct MoveTarget
{
float score;
float scorePerTurn;
std::vector<BattleHex> positions;
std::optional<AttackPossibility> cachedAttack;
uint8_t turnsToRich;
@ -64,7 +63,7 @@ struct MoveTarget
struct EvaluationResult
{
static const int64_t INEFFECTIVE_SCORE = -10000;
static const int64_t INEFFECTIVE_SCORE = -100000000;
AttackPossibility bestAttack;
MoveTarget bestMove;
@ -113,13 +112,15 @@ private:
struct ReachabilityData
{
std::vector<const battle::Unit *> units;
std::map<int, std::vector<const battle::Unit *>> units;
// shooters which are within mellee attack and mellee units
std::vector<const battle::Unit *> melleeAccessible;
// far shooters
std::vector<const battle::Unit *> shooters;
std::set<uint32_t> enemyUnitsReachingAttacker;
};
class BattleExchangeEvaluator
@ -131,6 +132,7 @@ private:
std::map<BattleHex, std::vector<const battle::Unit *>> reachabilityMap;
std::vector<battle::Units> turnOrder;
float negativeEffectMultiplier;
int simulationTurnsCount;
float scoreValue(const BattleScore & score) const;
@ -139,7 +141,8 @@ private:
uint8_t turn,
PotentialTargets & targets,
DamageCache & damageCache,
std::shared_ptr<HypotheticBattle> hb) const;
std::shared_ptr<HypotheticBattle> hb,
std::vector<const battle::Unit *> additionalUnits = {}) const;
bool canBeHitThisTurn(const AttackPossibility & ap);
@ -147,15 +150,17 @@ public:
BattleExchangeEvaluator(
std::shared_ptr<CBattleInfoCallback> cb,
std::shared_ptr<Environment> env,
float strengthRatio): cb(cb), env(env) {
negativeEffectMultiplier = strengthRatio >= 1 ? 1 : strengthRatio;
float strengthRatio,
int simulationTurnsCount): cb(cb), env(env), simulationTurnsCount(simulationTurnsCount){
negativeEffectMultiplier = strengthRatio >= 1 ? 1 : strengthRatio * strengthRatio;
}
EvaluationResult findBestTarget(
const battle::Unit * activeStack,
PotentialTargets & targets,
DamageCache & damageCache,
std::shared_ptr<HypotheticBattle> hb);
std::shared_ptr<HypotheticBattle> hb,
bool siegeDefense = false);
float evaluateExchange(
const AttackPossibility & ap,
@ -171,7 +176,8 @@ public:
const AttackPossibility & ap,
uint8_t turn,
PotentialTargets & targets,
std::shared_ptr<HypotheticBattle> hb) const;
std::shared_ptr<HypotheticBattle> hb,
std::vector<const battle::Unit *> additionalUnits = {}) const;
bool checkPositionBlocksOurStacks(HypotheticBattle & hb, const battle::Unit * unit, BattleHex position);

View File

@ -37,11 +37,7 @@ else()
endif()
target_include_directories(BattleAI PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(BattleAI PRIVATE vcmi TBB::tbb)
target_link_libraries(BattleAI PRIVATE vcmi)
vcmi_set_output_dir(BattleAI "AI")
enable_pch(BattleAI)
if(APPLE_IOS AND NOT USING_CONAN)
install(IMPORTED_RUNTIME_ARTIFACTS TBB::tbb LIBRARY DESTINATION ${LIB_DIR}) # CMake 3.21+
endif()

View File

@ -10,6 +10,7 @@
#include "StdInc.h"
#include "PotentialTargets.h"
#include "../../lib/CStack.h"//todo: remove
#include "../../lib/mapObjects/CGTownInstance.h"
PotentialTargets::PotentialTargets(
const battle::Unit * attacker,

View File

@ -12,6 +12,7 @@
#include <vcmi/events/EventBus.h>
#include "../../lib/battle/BattleLayout.h"
#include "../../lib/CStack.h"
#include "../../lib/ScriptHandler.h"
#include "../../lib/networkPacks/PacksForClientBattle.h"
@ -116,7 +117,7 @@ uint32_t StackWithBonuses::unitId() const
return id;
}
ui8 StackWithBonuses::unitSide() const
BattleSide StackWithBonuses::unitSide() const
{
return side;
}
@ -132,10 +133,10 @@ SlotID StackWithBonuses::unitSlot() const
}
TConstBonusListPtr StackWithBonuses::getAllBonuses(const CSelector & selector, const CSelector & limit,
const CBonusSystemNode * root, const std::string & cachingStr) const
const std::string & cachingStr) const
{
auto ret = std::make_shared<BonusList>();
TConstBonusListPtr originalList = origBearer->getAllBonuses(selector, limit, root, cachingStr);
TConstBonusListPtr originalList = origBearer->getAllBonuses(selector, limit, cachingStr);
vstd::copy_if(*originalList, std::back_inserter(*ret), [this](const std::shared_ptr<Bonus> & b)
{
@ -467,7 +468,7 @@ int64_t HypotheticBattle::getActualDamage(const DamageRange & damage, int32_t at
return (damage.min + damage.max) / 2;
}
std::vector<SpellID> HypotheticBattle::getUsedSpells(ui8 side) const
std::vector<SpellID> HypotheticBattle::getUsedSpells(BattleSide side) const
{
// TODO
return {};
@ -479,10 +480,9 @@ int3 HypotheticBattle::getLocation() const
return int3(-1, -1, -1);
}
bool HypotheticBattle::isCreatureBank() const
BattleLayout HypotheticBattle::getLayout() const
{
// TODO
return false;
return subject->getBattle()->getLayout();
}
int64_t HypotheticBattle::getTreeVersion() const
@ -502,10 +502,18 @@ ServerCallback * HypotheticBattle::getServerCallback()
return serverCallback.get();
}
void HypotheticBattle::makeWait(const battle::Unit * activeStack)
{
auto unit = getForUpdate(activeStack->unitId());
resetActiveUnit();
unit->waiting = true;
unit->waitedThisTurn = true;
}
HypotheticBattle::HypotheticServerCallback::HypotheticServerCallback(HypotheticBattle * owner_)
:owner(owner_)
{
}
void HypotheticBattle::HypotheticServerCallback::complain(const std::string & problem)
@ -523,44 +531,44 @@ vstd::RNG * HypotheticBattle::HypotheticServerCallback::getRNG()
return &rngStub;
}
void HypotheticBattle::HypotheticServerCallback::apply(CPackForClient * pack)
void HypotheticBattle::HypotheticServerCallback::apply(CPackForClient & pack)
{
logAi->error("Package of type %s is not allowed in battle evaluation", typeid(pack).name());
}
void HypotheticBattle::HypotheticServerCallback::apply(BattleLogMessage * pack)
void HypotheticBattle::HypotheticServerCallback::apply(BattleLogMessage & pack)
{
pack->applyBattle(owner);
pack.applyBattle(owner);
}
void HypotheticBattle::HypotheticServerCallback::apply(BattleStackMoved * pack)
void HypotheticBattle::HypotheticServerCallback::apply(BattleStackMoved & pack)
{
pack->applyBattle(owner);
pack.applyBattle(owner);
}
void HypotheticBattle::HypotheticServerCallback::apply(BattleUnitsChanged * pack)
void HypotheticBattle::HypotheticServerCallback::apply(BattleUnitsChanged & pack)
{
pack->applyBattle(owner);
pack.applyBattle(owner);
}
void HypotheticBattle::HypotheticServerCallback::apply(SetStackEffect * pack)
void HypotheticBattle::HypotheticServerCallback::apply(SetStackEffect & pack)
{
pack->applyBattle(owner);
pack.applyBattle(owner);
}
void HypotheticBattle::HypotheticServerCallback::apply(StacksInjured * pack)
void HypotheticBattle::HypotheticServerCallback::apply(StacksInjured & pack)
{
pack->applyBattle(owner);
pack.applyBattle(owner);
}
void HypotheticBattle::HypotheticServerCallback::apply(BattleObstaclesChanged * pack)
void HypotheticBattle::HypotheticServerCallback::apply(BattleObstaclesChanged & pack)
{
pack->applyBattle(owner);
pack.applyBattle(owner);
}
void HypotheticBattle::HypotheticServerCallback::apply(CatapultAttack * pack)
void HypotheticBattle::HypotheticServerCallback::apply(CatapultAttack & pack)
{
pack->applyBattle(owner);
pack.applyBattle(owner);
}
HypotheticBattle::HypotheticEnvironment::HypotheticEnvironment(HypotheticBattle * owner_, const Environment * upperEnvironment)

View File

@ -21,23 +21,43 @@
class HypotheticBattle;
///Fake random generator, used by AI to evaluate random server behavior
class RNGStub : public vstd::RNG
class RNGStub final : public vstd::RNG
{
public:
vstd::TRandI64 getInt64Range(int64_t lower, int64_t upper) override
int nextInt() override
{
return [=]()->int64_t
{
return (lower + upper)/2;
};
return 0;
}
vstd::TRand getDoubleRange(double lower, double upper) override
int nextBinomialInt(int coinsCount, double coinChance) override
{
return [=]()->double
{
return (lower + upper)/2;
};
return coinsCount * coinChance;
}
int nextInt(int lower, int upper) override
{
return (lower + upper) / 2;
}
int64_t nextInt64(int64_t lower, int64_t upper) override
{
return (lower + upper) / 2;
}
double nextDouble(double lower, double upper) override
{
return (lower + upper) / 2;
}
int nextInt(int upper) override
{
return upper / 2;
}
int64_t nextInt64(int64_t upper) override
{
return upper / 2;
}
double nextDouble(double upper) override
{
return upper / 2;
}
};
@ -65,13 +85,13 @@ public:
int32_t unitBaseAmount() const override;
uint32_t unitId() const override;
ui8 unitSide() const override;
BattleSide unitSide() const override;
PlayerColor unitOwner() const override;
SlotID unitSlot() const override;
///IBonusBearer
TConstBonusListPtr getAllBonuses(const CSelector & selector, const CSelector & limit,
const CBonusSystemNode * root = nullptr, const std::string & cachingStr = "") const override;
const std::string & cachingStr = "") const override;
int64_t getTreeVersion() const override;
@ -91,7 +111,7 @@ private:
const CCreature * type;
ui32 baseAmount;
uint32_t id;
ui8 side;
BattleSide side;
PlayerColor player;
SlotID slot;
};
@ -138,12 +158,19 @@ public:
uint32_t nextUnitId() const override;
int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const override;
std::vector<SpellID> getUsedSpells(ui8 side) const override;
std::vector<SpellID> getUsedSpells(BattleSide side) const override;
int3 getLocation() const override;
bool isCreatureBank() const override;
BattleLayout getLayout() const override;
int64_t getTreeVersion() const;
void makeWait(const battle::Unit * activeStack);
void resetActiveUnit()
{
activeUnitId = -1;
}
#if SCRIPTING_ENABLED
scripting::Pool * getContextPool() const override;
#endif
@ -162,15 +189,15 @@ private:
vstd::RNG * getRNG() override;
void apply(CPackForClient * pack) override;
void apply(CPackForClient & pack) override;
void apply(BattleLogMessage * pack) override;
void apply(BattleStackMoved * pack) override;
void apply(BattleUnitsChanged * pack) override;
void apply(SetStackEffect * pack) override;
void apply(StacksInjured * pack) override;
void apply(BattleObstaclesChanged * pack) override;
void apply(CatapultAttack * pack) override;
void apply(BattleLogMessage & pack) override;
void apply(BattleStackMoved & pack) override;
void apply(BattleUnitsChanged & pack) override;
void apply(SetStackEffect & pack) override;
void apply(StacksInjured & pack) override;
void apply(BattleObstaclesChanged & pack) override;
void apply(CatapultAttack & pack) override;
private:
HypotheticBattle * owner;
RNGStub rngStub;

View File

@ -70,4 +70,4 @@ ThreatMap::ThreatMap(const CStack *Endangered) : endangered(Endangered)
});
}
}
*/ // These lines may be usefull but they are't used in the code.
*/ // These lines may be useful but they are't used in the code.

View File

@ -22,4 +22,4 @@ public:
std::array<int, GameConstants::BFIELD_SIZE> sufferedDamage;
ThreatMap(const CStack *Endangered);
};*/ // These lines may be usefull but they are't used in the code.
};*/ // These lines may be useful but they are't used in the code.

View File

@ -8,10 +8,6 @@ else()
option(FORCE_BUNDLED_FL "Force to use FuzzyLite included into VCMI's source tree" OFF)
endif()
if(TBB_FOUND AND MSVC)
install_vcpkg_imported_tgt(TBB::tbb)
endif()
#FuzzyLite uses MSVC pragmas in headers, so, we need to disable -Wunknown-pragmas
if(MINGW)
add_compile_options(-Wno-unknown-pragmas)

View File

@ -14,14 +14,6 @@
#include "../../lib/CStack.h"
#include "../../lib/battle/BattleAction.h"
void CEmptyAI::saveGame(BinarySerializer & h)
{
}
void CEmptyAI::loadGame(BinaryDeserializer & h)
{
}
void CEmptyAI::initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB)
{
cb = CB;

View File

@ -19,9 +19,6 @@ class CEmptyAI : public CGlobalAI
std::shared_ptr<CCallback> cb;
public:
void saveGame(BinarySerializer & h) override;
void loadGame(BinaryDeserializer & h) override;
void initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB) override;
void yourTurn(QueryID queryID) override;
void yourTacticPhase(const BattleID & battleID, int distance) override;

View File

@ -1,86 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<CodeBlocks_project_file>
<FileVersion major="1" minor="6" />
<Project>
<Option title="EmptyAI" />
<Option pch_mode="2" />
<Option compiler="gcc" />
<Build>
<Target title="Debug-win32">
<Option platforms="Windows;" />
<Option output="../EmptyAI" imp_lib="$(TARGET_OUTPUT_DIR)$(TARGET_OUTPUT_BASENAME).a" def_file="$(TARGET_OUTPUT_DIR)$(TARGET_OUTPUT_BASENAME).def" prefix_auto="1" extension_auto="1" />
<Option object_output="obj/Debug/x86/" />
<Option type="3" />
<Option compiler="gcc" />
<Compiler>
<Add option="-ggdb" />
</Compiler>
<Linker>
<Add option="-lboost_system$(#boost.libsuffix32)" />
<Add option="-lVCMI_lib" />
<Add directory="$(#boost.lib32)" />
</Linker>
</Target>
<Target title="Release-win32">
<Option platforms="Windows;" />
<Option output="../EmptyAI" imp_lib="$(TARGET_OUTPUT_DIR)$(TARGET_OUTPUT_BASENAME).a" def_file="$(TARGET_OUTPUT_DIR)$(TARGET_OUTPUT_BASENAME).def" prefix_auto="1" extension_auto="1" />
<Option object_output="obj/Release/x86/" />
<Option type="3" />
<Option compiler="gcc" />
<Compiler>
<Add option="-O2" />
</Compiler>
<Linker>
<Add option="-lboost_system$(#boost.libsuffix32)" />
<Add option="-lVCMI_lib" />
<Add directory="$(#boost.lib32)" />
</Linker>
</Target>
<Target title="Debug-win64">
<Option platforms="Windows;" />
<Option output="../EmptyAI" imp_lib="$(TARGET_OUTPUT_DIR)$(TARGET_OUTPUT_BASENAME).a" def_file="$(TARGET_OUTPUT_DIR)$(TARGET_OUTPUT_BASENAME).def" prefix_auto="1" extension_auto="1" />
<Option object_output="obj/Debug/x64/" />
<Option type="3" />
<Option compiler="gnu_gcc_compiler_x64" />
<Compiler>
<Add option="-ggdb" />
</Compiler>
<Linker>
<Add option="-lboost_system$(#boost.libsuffix64)" />
<Add option="-lVCMI_lib" />
<Add directory="$(#boost.lib64)" />
</Linker>
</Target>
</Build>
<Compiler>
<Add option="-Wextra" />
<Add option="-Wall" />
<Add option="-std=gnu++11" />
<Add option="-fexceptions" />
<Add option="-Wpointer-arith" />
<Add option="-Wno-switch" />
<Add option="-Wno-sign-compare" />
<Add option="-Wno-unused-parameter" />
<Add option="-Wno-overloaded-virtual" />
<Add option="-fpermissive" />
<Add option="-D_WIN32_WINNT=0x0600" />
<Add option="-D_WIN32" />
<Add option="-DBOOST_ALL_DYN_LINK" />
<Add directory="$(#boost.include)" />
<Add directory="../../include" />
</Compiler>
<Linker>
<Add directory="../.." />
</Linker>
<Unit filename="CEmptyAI.cpp" />
<Unit filename="CEmptyAI.h" />
<Unit filename="StdInc.h">
<Option compile="1" />
<Option weight="0" />
</Unit>
<Unit filename="exp_funcs.cpp" />
<Extensions>
<lib_finder disable_auto="1" />
</Extensions>
</Project>
</CodeBlocks_project_file>

View File

@ -1,181 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="RD|Win32">
<Configuration>RD</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="RD|x64">
<Configuration>RD</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<ItemGroup>
<ClCompile Include="CEmptyAI.cpp" />
<ClCompile Include="exp_funcs.cpp" />
<ClCompile Include="StdInc.cpp">
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='RD|Win32'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='RD|x64'">Create</PrecompiledHeader>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="CEmptyAI.h" />
<ClInclude Include="StdInc.h" />
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{C41C4EB6-6F74-4F37-9FB0-9FA6BF377837}</ProjectGuid>
<RootNamespace>EmptyAI</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>MultiByte</CharacterSet>
<PlatformToolset>v140_xp</PlatformToolset>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>MultiByte</CharacterSet>
<PlatformToolset>v140_xp</PlatformToolset>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='RD|Win32'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>MultiByte</CharacterSet>
<PlatformToolset>v142</PlatformToolset>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='RD|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>MultiByte</CharacterSet>
<PlatformToolset>v140_xp</PlatformToolset>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="..\..\VCMI_global_debug.props" />
<Import Project="..\..\VCMI_global.props" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="..\..\VCMI_global_debug.props" />
<Import Project="..\..\VCMI_global.props" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='RD|Win32'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="..\..\VCMI_global_release.props" />
<Import Project="..\..\VCMI_global.props" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='RD|x64'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="..\..\VCMI_global_release.props" />
<Import Project="..\..\VCMI_global.props" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<OutDir>$(VCMI_Out)\AI\</OutDir>
<IncludePath>$(IncludePath)</IncludePath>
<LibraryPath>$(LibraryPath)</LibraryPath>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<OutDir>$(VCMI_Out)\AI\</OutDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='RD|Win32'">
<OutDir>$(VCMI_Out)/AI</OutDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='RD|x64'">
<OutDir>$(VCMI_Out)\AI\</OutDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>StdInc.h</PrecompiledHeaderFile>
<PreprocessorDefinitions>%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalOptions>/Zm130 %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>VCMI_lib.lib;%(AdditionalDependencies)</AdditionalDependencies>
<OutputFile>$(OutDir)EmptyAI.dll</OutputFile>
<AdditionalLibraryDirectories>..\..\..\libs;..\..</AdditionalLibraryDirectories>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<Optimization>Disabled</Optimization>
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>StdInc.h</PrecompiledHeaderFile>
<PreprocessorDefinitions>_WINDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<GenerateDebugInformation>true</GenerateDebugInformation>
<AdditionalDependencies>VCMI_lib.lib;%(AdditionalDependencies)</AdditionalDependencies>
<OutputFile>$(OutDir)EmptyAI.dll</OutputFile>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='RD|Win32'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>StdInc.h</PrecompiledHeaderFile>
<PreprocessorDefinitions>_WINDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
</ClCompile>
<Link>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<AdditionalDependencies>VCMI_lib.lib;%(AdditionalDependencies)</AdditionalDependencies>
<OutputFile>$(OutDir)EmptyAI.dll</OutputFile>
<AdditionalLibraryDirectories>$(VCMI_Out)</AdditionalLibraryDirectories>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='RD|x64'">
<ClCompile>
<WarningLevel>Level3</WarningLevel>
<Optimization>MaxSpeed</Optimization>
<FunctionLevelLinking>true</FunctionLevelLinking>
<IntrinsicFunctions>true</IntrinsicFunctions>
<AdditionalIncludeDirectories>%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>StdInc.h</PrecompiledHeaderFile>
<PreprocessorDefinitions>_WINDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalOptions>/Zm130 %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
<GenerateDebugInformation>true</GenerateDebugInformation>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
<AdditionalDependencies>VCMI_lib.lib;%(AdditionalDependencies)</AdditionalDependencies>
<OutputFile>$(OutDir)EmptyAI.dll</OutputFile>
</Link>
</ItemDefinitionGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

View File

@ -1,285 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<CodeBlocks_project_file>
<FileVersion major="1" minor="6" />
<Project>
<Option title="FuzzyLite" />
<Option pch_mode="2" />
<Option compiler="gcc" />
<Build>
<Target title="Debug-win32">
<Option platforms="Windows;" />
<Option output="FuzzyLite" prefix_auto="1" extension_auto="1" />
<Option working_dir="" />
<Option object_output="../obj/FuzzyLite/Debug/x86" />
<Option type="2" />
<Option compiler="gcc" />
<Option createDefFile="1" />
<Compiler>
<Add option="-Og" />
<Add option="-g" />
</Compiler>
</Target>
<Target title="Release-win32">
<Option platforms="Windows;" />
<Option output="FuzzyLite" prefix_auto="1" extension_auto="1" />
<Option working_dir="" />
<Option object_output="../obj/FuzzyLite/Release/x86" />
<Option type="2" />
<Option compiler="gcc" />
<Option createDefFile="1" />
<Compiler>
<Add option="-fomit-frame-pointer" />
<Add option="-O2" />
</Compiler>
<Linker>
<Add option="-s" />
</Linker>
</Target>
<Target title="Debug-win64">
<Option platforms="Windows;" />
<Option output="FuzzyLite" prefix_auto="1" extension_auto="1" />
<Option working_dir="" />
<Option object_output="../obj/FuzzyLite/Debug/x64" />
<Option type="2" />
<Option compiler="gnu_gcc_compiler_x64" />
<Option createDefFile="1" />
<Compiler>
<Add option="-Og" />
<Add option="-g" />
</Compiler>
</Target>
</Build>
<Compiler>
<Add option="-Wextra" />
<Add option="-Wall" />
<Add option="-std=gnu++11" />
<Add option="-fexceptions" />
<Add option="-Wpointer-arith" />
<Add option="-Wno-switch" />
<Add option="-Wno-sign-compare" />
<Add option="-Wno-unused-parameter" />
<Add option="-Wno-overloaded-virtual" />
<Add option="-DFL_CPP11" />
<Add directory="FuzzyLite/fuzzylite" />
</Compiler>
<Unit filename="FuzzyLite/fuzzylite/fl/Benchmark.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/Complexity.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/Console.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/Engine.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/Exception.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/Headers.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/Operation.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/activation/Activation.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/activation/First.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/activation/General.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/activation/Highest.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/activation/Last.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/activation/Lowest.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/activation/Proportional.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/activation/Threshold.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/defuzzifier/Bisector.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/defuzzifier/Centroid.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/defuzzifier/Defuzzifier.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/defuzzifier/IntegralDefuzzifier.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/defuzzifier/LargestOfMaximum.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/defuzzifier/MeanOfMaximum.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/defuzzifier/SmallestOfMaximum.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/defuzzifier/WeightedAverage.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/defuzzifier/WeightedAverageCustom.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/defuzzifier/WeightedDefuzzifier.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/defuzzifier/WeightedSum.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/defuzzifier/WeightedSumCustom.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/factory/ActivationFactory.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/factory/CloningFactory.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/factory/ConstructionFactory.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/factory/DefuzzifierFactory.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/factory/FactoryManager.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/factory/FunctionFactory.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/factory/HedgeFactory.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/factory/SNormFactory.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/factory/TNormFactory.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/factory/TermFactory.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/fuzzylite.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/hedge/Any.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/hedge/Extremely.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/hedge/Hedge.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/hedge/HedgeFunction.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/hedge/Not.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/hedge/Seldom.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/hedge/Somewhat.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/hedge/Very.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/imex/CppExporter.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/imex/Exporter.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/imex/FclExporter.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/imex/FclImporter.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/imex/FisExporter.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/imex/FisImporter.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/imex/FldExporter.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/imex/FllExporter.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/imex/FllImporter.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/imex/Importer.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/imex/JavaExporter.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/imex/RScriptExporter.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/norm/Norm.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/norm/SNorm.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/norm/TNorm.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/norm/s/AlgebraicSum.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/norm/s/BoundedSum.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/norm/s/DrasticSum.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/norm/s/EinsteinSum.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/norm/s/HamacherSum.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/norm/s/Maximum.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/norm/s/NilpotentMaximum.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/norm/s/NormalizedSum.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/norm/s/SNormFunction.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/norm/s/UnboundedSum.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/norm/t/AlgebraicProduct.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/norm/t/BoundedDifference.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/norm/t/DrasticProduct.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/norm/t/EinsteinProduct.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/norm/t/HamacherProduct.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/norm/t/Minimum.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/norm/t/NilpotentMinimum.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/norm/t/TNormFunction.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/rule/Antecedent.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/rule/Consequent.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/rule/Expression.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/rule/Rule.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/rule/RuleBlock.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/term/Activated.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/term/Aggregated.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/term/Bell.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/term/Binary.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/term/Concave.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/term/Constant.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/term/Cosine.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/term/Discrete.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/term/Function.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/term/Gaussian.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/term/GaussianProduct.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/term/Linear.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/term/PiShape.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/term/Ramp.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/term/Rectangle.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/term/SShape.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/term/Sigmoid.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/term/SigmoidDifference.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/term/SigmoidProduct.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/term/Spike.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/term/Term.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/term/Trapezoid.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/term/Triangle.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/term/ZShape.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/variable/InputVariable.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/variable/OutputVariable.h" />
<Unit filename="FuzzyLite/fuzzylite/fl/variable/Variable.h" />
<Unit filename="FuzzyLite/fuzzylite/src/Benchmark.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/Complexity.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/Console.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/Engine.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/Exception.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/activation/First.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/activation/General.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/activation/Highest.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/activation/Last.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/activation/Lowest.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/activation/Proportional.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/activation/Threshold.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/defuzzifier/Bisector.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/defuzzifier/Centroid.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/defuzzifier/IntegralDefuzzifier.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/defuzzifier/LargestOfMaximum.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/defuzzifier/MeanOfMaximum.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/defuzzifier/SmallestOfMaximum.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/defuzzifier/WeightedAverage.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/defuzzifier/WeightedAverageCustom.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/defuzzifier/WeightedDefuzzifier.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/defuzzifier/WeightedSum.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/defuzzifier/WeightedSumCustom.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/factory/ActivationFactory.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/factory/DefuzzifierFactory.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/factory/FactoryManager.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/factory/FunctionFactory.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/factory/HedgeFactory.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/factory/SNormFactory.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/factory/TNormFactory.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/factory/TermFactory.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/fuzzylite.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/hedge/Any.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/hedge/Extremely.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/hedge/HedgeFunction.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/hedge/Not.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/hedge/Seldom.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/hedge/Somewhat.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/hedge/Very.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/imex/CppExporter.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/imex/Exporter.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/imex/FclExporter.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/imex/FclImporter.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/imex/FisExporter.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/imex/FisImporter.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/imex/FldExporter.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/imex/FllExporter.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/imex/FllImporter.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/imex/Importer.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/imex/JavaExporter.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/imex/RScriptExporter.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/main.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/norm/s/AlgebraicSum.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/norm/s/BoundedSum.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/norm/s/DrasticSum.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/norm/s/EinsteinSum.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/norm/s/HamacherSum.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/norm/s/Maximum.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/norm/s/NilpotentMaximum.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/norm/s/NormalizedSum.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/norm/s/SNormFunction.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/norm/s/UnboundedSum.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/norm/t/AlgebraicProduct.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/norm/t/BoundedDifference.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/norm/t/DrasticProduct.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/norm/t/EinsteinProduct.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/norm/t/HamacherProduct.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/norm/t/Minimum.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/norm/t/NilpotentMinimum.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/norm/t/TNormFunction.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/rule/Antecedent.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/rule/Consequent.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/rule/Expression.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/rule/Rule.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/rule/RuleBlock.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/term/Activated.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/term/Aggregated.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/term/Bell.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/term/Binary.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/term/Concave.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/term/Constant.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/term/Cosine.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/term/Discrete.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/term/Function.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/term/Gaussian.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/term/GaussianProduct.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/term/Linear.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/term/PiShape.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/term/Ramp.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/term/Rectangle.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/term/SShape.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/term/Sigmoid.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/term/SigmoidDifference.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/term/SigmoidProduct.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/term/Spike.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/term/Term.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/term/Trapezoid.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/term/Triangle.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/term/ZShape.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/variable/InputVariable.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/variable/OutputVariable.cpp" />
<Unit filename="FuzzyLite/fuzzylite/src/variable/Variable.cpp" />
<Extensions>
<code_completion />
<envvars />
<debugger />
<lib_finder disable_auto="1" />
</Extensions>
</Project>
</CodeBlocks_project_file>

View File

@ -12,22 +12,21 @@
#include "../../lib/ArtifactUtils.h"
#include "../../lib/UnlockGuard.h"
#include "../../lib/StartInfo.h"
#include "../../lib/entities/building/CBuilding.h"
#include "../../lib/mapObjects/MapObjects.h"
#include "../../lib/mapObjects/ObjectTemplate.h"
#include "../../lib/mapObjects/CGHeroInstance.h"
#include "../../lib/CConfigHandler.h"
#include "../../lib/CHeroHandler.h"
#include "../../lib/GameSettings.h"
#include "../../lib/IGameSettings.h"
#include "../../lib/gameState/CGameState.h"
#include "../../lib/serializer/CTypeList.h"
#include "../../lib/serializer/BinarySerializer.h"
#include "../../lib/serializer/BinaryDeserializer.h"
#include "../../lib/networkPacks/PacksForClient.h"
#include "../../lib/networkPacks/PacksForClientBattle.h"
#include "../../lib/networkPacks/PacksForServer.h"
#include "../../lib/networkPacks/StackLocation.h"
#include "../../lib/battle/BattleStateInfoForRetreat.h"
#include "../../lib/battle/BattleInfo.h"
#include "../../lib/CPlayerState.h"
#include "AIGateway.h"
#include "Goals/Goals.h"
@ -35,11 +34,6 @@
namespace NKAI
{
// our to enemy strength ratio constants
const float SAFE_ATTACK_CONSTANT = 1.1f;
const float RETREAT_THRESHOLD = 0.3f;
const double RETREAT_ABSOLUTE_THRESHOLD = 10000.;
//one thread may be turn of AI and another will be handling a side effect for AI2
thread_local CCallback * cb = nullptr;
thread_local AIGateway * ai = nullptr;
@ -287,6 +281,9 @@ void AIGateway::tileRevealed(const std::unordered_set<int3> & pos)
for(const CGObjectInstance * obj : myCb->getVisitableObjs(tile))
addVisitableObj(obj);
}
if (nullkiller->settings->isUpdateHitmapOnTileReveal())
nullkiller->dangerHitMap->reset();
}
void AIGateway::heroExchangeStarted(ObjectInstanceID hero1, ObjectInstanceID hero2, QueryID query)
@ -500,7 +497,7 @@ void AIGateway::objectPropertyChanged(const SetObjectProperty * sop)
if(relations == PlayerRelations::ENEMIES)
{
//we want to visit objects owned by oppponents
//addVisitableObj(obj); // TODO: Remove once save compatability broken. In past owned objects were removed from this set
//addVisitableObj(obj); // TODO: Remove once save compatibility broken. In past owned objects were removed from this set
nullkiller->memory->markObjectUnvisited(obj);
}
else if(relations == PlayerRelations::SAME_PLAYER && obj->ID == Obj::TOWN)
@ -554,7 +551,7 @@ std::optional<BattleAction> AIGateway::makeSurrenderRetreatDecision(const Battle
double fightRatio = ourStrength / (double)battleState.getEnemyStrength();
// if we have no towns - things are already bad, so retreat is not an option.
if(cb->getTownsInfo().size() && ourStrength < RETREAT_ABSOLUTE_THRESHOLD && fightRatio < RETREAT_THRESHOLD && battleState.canFlee)
if(cb->getTownsInfo().size() && ourStrength < nullkiller->settings->getRetreatThresholdAbsolute() && fightRatio < nullkiller->settings->getRetreatThresholdRelative() && battleState.canFlee)
{
return BattleAction::makeRetreat(battleState.ourSide);
}
@ -568,6 +565,7 @@ void AIGateway::initGameInterface(std::shared_ptr<Environment> env, std::shared_
LOG_TRACE(logAi);
myCb = CB;
cbc = CB;
this->env = env;
NET_EVENT_HANDLER;
playerID = *myCb->getPlayerID();
@ -603,7 +601,7 @@ void AIGateway::heroGotLevel(const CGHeroInstance * hero, PrimarySkill pskill, s
if(hPtr.validAndSet())
{
std::unique_lock<std::mutex> lockGuard(nullkiller->aiStateMutex);
std::unique_lock lockGuard(nullkiller->aiStateMutex);
nullkiller->heroManager->update();
@ -648,7 +646,14 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector<C
auto danger = nullkiller->dangerEvaluator->evaluateDanger(target, hero.get());
auto ratio = static_cast<float>(danger) / hero->getTotalStrength();
answer = topObj->id == goalObjectID; // no if we do not aim to visit this object
answer = true;
if(topObj->id != goalObjectID && nullkiller->dangerEvaluator->evaluateDanger(topObj) > 0)
{
// no if we do not aim to visit this object
answer = false;
}
logAi->trace("Query hook: %s(%s) by %s danger ratio %f", target.toString(), topObj->getObjectName(), hero.name(), ratio);
if(cb->getObj(goalObjectID, false))
@ -663,7 +668,7 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector<C
else if(objType == Obj::ARTIFACT || objType == Obj::RESOURCE)
{
bool dangerUnknown = danger == 0;
bool dangerTooHigh = ratio > (1 / SAFE_ATTACK_CONSTANT);
bool dangerTooHigh = ratio * nullkiller->settings->getSafeAttackRatio() > 1;
answer = !dangerUnknown && !dangerTooHigh;
}
@ -683,7 +688,7 @@ void AIGateway::showBlockingDialog(const std::string & text, const std::vector<C
sel = components.size();
{
std::unique_lock<std::mutex> mxLock(nullkiller->aiStateMutex);
std::unique_lock mxLock(nullkiller->aiStateMutex);
// TODO: Find better way to understand it is Chest of Treasures
if(hero.validAndSet()
@ -705,7 +710,7 @@ void AIGateway::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelI
NET_EVENT_HANDLER;
status.addQuery(askID, boost::str(boost::format("Teleport dialog query with %d exits") % exits.size()));
int choosenExit = -1;
int chosenExit = -1;
if(impassable)
{
nullkiller->memory->knownTeleportChannels[channel]->passability = TeleportChannel::IMPASSABLE;
@ -714,14 +719,14 @@ void AIGateway::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelI
{
auto neededExit = std::make_pair(destinationTeleport, destinationTeleportPos);
if(destinationTeleport != ObjectInstanceID() && vstd::contains(exits, neededExit))
choosenExit = vstd::find_pos(exits, neededExit);
chosenExit = vstd::find_pos(exits, neededExit);
}
for(auto exit : exits)
{
if(status.channelProbing() && exit.first == destinationTeleport)
{
choosenExit = vstd::find_pos(exits, exit);
chosenExit = vstd::find_pos(exits, exit);
break;
}
else
@ -739,7 +744,7 @@ void AIGateway::showTeleportDialog(const CGHeroInstance * hero, TeleportChannelI
requestActionASAP([=]()
{
answerQuery(askID, choosenExit);
answerQuery(askID, chosenExit);
});
}
@ -756,7 +761,7 @@ void AIGateway::showGarrisonDialog(const CArmedInstance * up, const CGHeroInstan
//you can't request action from action-response thread
requestActionASAP([=]()
{
if(removableUnits && up->tempOwner == down->tempOwner && nullkiller->settings->isGarrisonTroopsUsageAllowed() && !cb->getStartInfo()->isSteadwickFallCampaignMission())
if(removableUnits && up->tempOwner == down->tempOwner && nullkiller->settings->isGarrisonTroopsUsageAllowed() && !cb->getStartInfo()->isRestorationOfErathiaCampaign())
{
pickBestCreatures(down, up);
}
@ -772,27 +777,6 @@ void AIGateway::showMapObjectSelectDialog(QueryID askID, const Component & icon,
requestActionASAP([=](){ answerQuery(askID, selectedObject.getNum()); });
}
void AIGateway::saveGame(BinarySerializer & h)
{
NET_EVENT_HANDLER;
nullkiller->memory->removeInvisibleObjects(myCb.get());
CAdventureAI::saveGame(h);
serializeInternal(h);
}
void AIGateway::loadGame(BinaryDeserializer & h)
{
//NET_EVENT_HANDLER;
#if 0
//disabled due to issue 2890
registerGoals(h);
#endif // 0
CAdventureAI::loadGame(h);
serializeInternal(h);
}
bool AIGateway::makePossibleUpgrades(const CArmedInstance * obj)
{
if(!obj)
@ -825,7 +809,7 @@ void AIGateway::makeTurn()
auto day = cb->getDate(Date::DAY);
logAi->info("Player %d (%s) starting turn, day %d", playerID, playerID.toString(), day);
boost::shared_lock<boost::shared_mutex> gsLock(CGameState::mutex);
boost::shared_lock gsLock(CGameState::mutex);
setThreadName("AIGateway::makeTurn");
if(nullkiller->isOpenMap())
@ -877,7 +861,7 @@ void AIGateway::makeTurn()
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->anchorPos().toString());
switch(obj->ID)
{
case Obj::TOWN:
@ -885,7 +869,7 @@ void AIGateway::performObjectInteraction(const CGObjectInstance * obj, HeroPtr h
{
makePossibleUpgrades(h.get());
std::unique_lock<std::mutex> lockGuard(nullkiller->aiStateMutex);
std::unique_lock lockGuard(nullkiller->aiStateMutex);
if(!h->visitedTown->garrisonHero || !nullkiller->isHeroLocked(h->visitedTown->garrisonHero))
moveCreaturesToHero(h->visitedTown);
@ -1069,7 +1053,7 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance
//FIXME: why are the above possible to be null?
bool emptySlotFound = false;
for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType()))
for(auto slot : artifact->getType()->getPossibleSlots().at(target->bearerType()))
{
if(target->isPositionFree(slot) && artifact->canBePutAt(target, slot, true)) //combined artifacts are not always allowed to move
{
@ -1082,7 +1066,7 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance
}
if(!emptySlotFound) //try to put that atifact in already occupied slot
{
for(auto slot : artifact->artType->getPossibleSlots().at(target->bearerType()))
for(auto slot : artifact->getType()->getPossibleSlots().at(target->bearerType()))
{
auto otherSlot = target->getSlot(slot);
if(otherSlot && otherSlot->artifact) //we need to exchange artifact for better one
@ -1093,8 +1077,8 @@ void AIGateway::pickBestArtifacts(const CGHeroInstance * h, const CGHeroInstance
{
logAi->trace(
"Exchange artifacts %s <-> %s",
artifact->artType->getNameTranslated(),
otherSlot->artifact->artType->getNameTranslated());
artifact->getType()->getNameTranslated(),
otherSlot->artifact->getType()->getNameTranslated());
if(!otherSlot->artifact->canBePutAt(artHolder, location.slot, true))
{
@ -1143,10 +1127,10 @@ void AIGateway::recruitCreatures(const CGDwelling * d, const CArmedInstance * re
{
for(auto stack : recruiter->Slots())
{
if(!stack.second->type)
if(!stack.second->getType())
continue;
auto duplicatingSlot = recruiter->getSlotFor(stack.second->type);
auto duplicatingSlot = recruiter->getSlotFor(stack.second->getCreature());
if(duplicatingSlot != stack.first)
{
@ -1167,7 +1151,7 @@ void AIGateway::recruitCreatures(const CGDwelling * d, const CArmedInstance * re
}
}
void AIGateway::battleStart(const BattleID & battleID, 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, BattleSide side, bool replayAllowed)
{
NET_EVENT_HANDLER;
assert(!playerID.isValidPlayer() || status.getBattle() == UPCOMING_BATTLE);
@ -1187,6 +1171,17 @@ void AIGateway::battleEnd(const BattleID & battleID, const BattleResult * br, Qu
battlename.clear();
CAdventureAI::battleEnd(battleID, br, queryID);
// gosolo
if(queryID != QueryID::NONE && myCb->getPlayerState(playerID)->isHuman())
{
status.addQuery(queryID, "Confirm battle query");
requestActionASAP([=]()
{
answerQuery(queryID, 0);
});
}
}
void AIGateway::waitTillFree()
@ -1319,6 +1314,11 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
auto doTeleportMovement = [&](ObjectInstanceID exitId, int3 exitPos)
{
if(cb->getObj(exitId) && cb->getObj(exitId)->ID == Obj::WHIRLPOOL)
{
nullkiller->armyFormation->rearrangeArmyForWhirlpool(*h);
}
destinationTeleport = exitId;
if(exitPos.valid())
destinationTeleportPos = exitPos;
@ -1340,6 +1340,7 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
status.setChannelProbing(true);
for(auto exit : teleportChannelProbingList)
doTeleportMovement(exit, int3(-1));
teleportChannelProbingList.clear();
status.setChannelProbing(false);
@ -1450,8 +1451,8 @@ bool AIGateway::moveHeroToTile(int3 dst, HeroPtr h)
void AIGateway::buildStructure(const CGTownInstance * t, BuildingID building)
{
auto name = t->town->buildings.at(building)->getNameTranslated();
logAi->debug("Player %d will build %s in town of %s at %s", ai->playerID, name, t->getNameTranslated(), t->pos.toString());
auto name = t->getTown()->buildings.at(building)->getNameTranslated();
logAi->debug("Player %d will build %s in town of %s at %s", ai->playerID, name, t->getNameTranslated(), t->anchorPos().toString());
cb->buildBuilding(t, building); //just do this;
}
@ -1473,7 +1474,7 @@ void AIGateway::tryRealize(Goals::Trade & g) //trade
if(cb->getResourceAmount(GameResID(g.resID)) >= g.value) //goal is already fulfilled. Why we need this check, anyway?
throw goalFulfilledException(sptr(g));
int accquiredResources = 0;
int acquiredResources = 0;
if(const CGObjectInstance * obj = cb->getObj(ObjectInstanceID(g.objid), false))
{
if(const auto * m = dynamic_cast<const IMarket*>(obj))
@ -1492,9 +1493,9 @@ void AIGateway::tryRealize(Goals::Trade & g) //trade
//TODO trade only as much as needed
if (toGive) //don't try to sell 0 resources
{
cb->trade(m, EMarketMode::RESOURCE_RESOURCE, res, GameResID(g.resID), toGive);
accquiredResources = static_cast<int>(toGet * (it->resVal / toGive));
logAi->debug("Traded %d of %s for %d of %s at %s", toGive, res, accquiredResources, g.resID, obj->getObjectName());
cb->trade(m->getObjInstanceID(), EMarketMode::RESOURCE_RESOURCE, res, GameResID(g.resID), toGive);
acquiredResources = static_cast<int>(toGet * (it->resVal / toGive));
logAi->debug("Traded %d of %s for %d of %s at %s", toGive, res, acquiredResources, g.resID, obj->getObjectName());
}
if (cb->getResourceAmount(GameResID(g.resID)))
throw goalFulfilledException(sptr(g)); //we traded all we needed
@ -1565,7 +1566,7 @@ void AIGateway::requestActionASAP(std::function<void()> whatToDo)
{
setThreadName("AIGateway::requestActionASAP::whatToDo");
SET_GLOBAL_STATE(this);
boost::shared_lock<boost::shared_mutex> gsLock(CGameState::mutex);
boost::shared_lock gsLock(CGameState::mutex);
whatToDo();
});

View File

@ -16,9 +16,7 @@
#include "../../lib/CThreadHelper.h"
#include "../../lib/GameConstants.h"
#include "../../lib/VCMI_Lib.h"
#include "../../lib/CBuildingHandler.h"
#include "../../lib/CCreatureHandler.h"
#include "../../lib/CTownHandler.h"
#include "../../lib/mapObjects/MiscObjects.h"
#include "../../lib/spells/CSpellHandler.h"
#include "Pathfinding/AIPathfinder.h"
@ -59,15 +57,6 @@ public:
void attemptedAnsweringQuery(QueryID queryID, int answerRequestID);
void receivedAnswerConfirmation(int answerRequestID, int result);
void heroVisit(const CGObjectInstance * obj, bool started);
template<typename Handler> void serialize(Handler & h)
{
h & battle;
h & remainingQueries;
h & requestToQueryID;
h & havingTurn;
}
};
// The gateway is responsible for AI events handling. Copied from VCAI.h and refined a bit
@ -104,7 +93,7 @@ public:
AIGateway();
virtual ~AIGateway();
//TODO: extract to apropriate goals
//TODO: extract to appropriate goals
void tryRealize(Goals::DigAtTile & g);
void tryRealize(Goals::Trade & g);
@ -119,8 +108,6 @@ public:
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(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override;
void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector<ObjectInstanceID> & objects) override;
void saveGame(BinarySerializer & h) override; //saving
void loadGame(BinaryDeserializer & h) override; //loading
void finish() override;
void availableCreaturesChanged(const CGDwelling * town) override;
@ -169,7 +156,7 @@ public:
void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override;
std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) 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 battleStart(const BattleID & battleID, const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, BattleSide side, bool replayAllowed) override;
void battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID) override;
void makeTurn();
@ -202,17 +189,6 @@ public:
void answerQuery(QueryID queryID, int selection);
//special function that can be called ONLY from game events handling thread and will send request ASAP
void requestActionASAP(std::function<void()> whatToDo);
template<typename Handler> void serializeInternal(Handler & h)
{
h & nullkiller->memory->knownTeleportChannels;
h & nullkiller->memory->knownSubterraneanGates;
h & destinationTeleport;
h & nullkiller->memory->visitableObjs;
h & nullkiller->memory->alreadyVisited;
h & status;
h & battlename;
}
};
}

View File

@ -14,11 +14,10 @@
#include "../../lib/UnlockGuard.h"
#include "../../lib/CConfigHandler.h"
#include "../../lib/CHeroHandler.h"
#include "../../lib/mapObjects/MapObjects.h"
#include "../../lib/mapping/CMapDefines.h"
#include "../../lib/gameState/QuestInfo.h"
#include "../../lib/GameSettings.h"
#include "../../lib/IGameSettings.h"
#include <vcmi/CreatureService.h>
@ -147,21 +146,21 @@ bool HeroPtr::operator==(const HeroPtr & rhs) const
return h == rhs.get(true);
}
bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet * heroArmy, uint64_t dangerStrength)
bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet * heroArmy, uint64_t dangerStrength, float safeAttackRatio)
{
const ui64 heroStrength = h->getFightingStrength() * heroArmy->getArmyStrength();
const ui64 heroStrength = h->getHeroStrength() * heroArmy->getArmyStrength();
if(dangerStrength)
{
return heroStrength / SAFE_ATTACK_CONSTANT > dangerStrength;
return heroStrength > dangerStrength * safeAttackRatio;
}
return true; //there's no danger
}
bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength)
bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength, float safeAttackRatio)
{
return isSafeToVisit(h, h, dangerStrength);
return isSafeToVisit(h, h, dangerStrength, safeAttackRatio);
}
bool isObjectRemovable(const CGObjectInstance * obj)
@ -194,7 +193,7 @@ bool canBeEmbarkmentPoint(const TerrainTile * t, bool fromWater)
{
// TODO: Such information should be provided by pathfinder
// Tile must be free or with unoccupied boat
if(!t->blocked)
if(!t->blocked())
{
return true;
}
@ -268,8 +267,8 @@ bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2)
bool compareArtifacts(const CArtifactInstance * a1, const CArtifactInstance * a2)
{
auto art1 = a1->artType;
auto art2 = a2->artType;
auto art1 = a1->getType();
auto art2 = a2->getType();
if(art1->getPrice() == art2->getPrice())
return art1->valOfBonuses(BonusType::PRIMARY_SKILL) > art2->valOfBonuses(BonusType::PRIMARY_SKILL);
@ -313,7 +312,7 @@ int getDuplicatingSlots(const CArmedInstance * army)
for(auto stack : army->Slots())
{
if(stack.second->type && army->getSlotFor(stack.second->type) != stack.first)
if(stack.second->getCreature() && army->getSlotFor(stack.second->getCreature()) != stack.first)
duplicatingSlots++;
}
@ -388,7 +387,7 @@ bool shouldVisit(const Nullkiller * ai, const CGHeroInstance * h, const CGObject
{
for(auto slot : h->Slots())
{
if(slot.second->type->hasUpgrades())
if(slot.second->getType()->hasUpgrades())
return true; //TODO: check price?
}
return false;
@ -430,9 +429,16 @@ bool shouldVisit(const Nullkiller * ai, const CGHeroInstance * h, const CGObject
return false;
}
if(obj->wasVisited(h)) //it must pointer to hero instance, heroPtr calls function wasVisited(ui8 player);
if(obj->wasVisited(h))
return false;
auto rewardable = dynamic_cast<const Rewardable::Interface *>(obj);
if(rewardable && rewardable->getAvailableRewards(h, Rewardable::EEventType::EVENT_FIRST_VISIT).empty())
{
return false;
}
return true;
}
@ -441,9 +447,9 @@ bool townHasFreeTavern(const CGTownInstance * town)
if(!town->hasBuilt(BuildingID::TAVERN)) return false;
if(!town->visitingHero) return true;
bool canMoveVisitingHeroToGarnison = !town->getUpperArmy()->stacksCount();
bool canMoveVisitingHeroToGarrison = !town->getUpperArmy()->stacksCount();
return canMoveVisitingHeroToGarnison;
return canMoveVisitingHeroToGarrison;
}
uint64_t getHeroArmyStrengthWithCommander(const CGHeroInstance * hero, const CCreatureSet * heroArmy)

View File

@ -40,9 +40,7 @@
/*********************** TBB.h ********************/
#include "../../lib/VCMI_Lib.h"
#include "../../lib/CBuildingHandler.h"
#include "../../lib/CCreatureHandler.h"
#include "../../lib/CTownHandler.h"
#include "../../lib/spells/CSpellHandler.h"
#include "../../lib/CStopWatch.h"
#include "../../lib/mapObjects/CGHeroInstance.h"
@ -63,11 +61,6 @@ const int GOLD_MINE_PRODUCTION = 1000;
const int WOOD_ORE_MINE_PRODUCTION = 2;
const int RESOURCE_MINE_PRODUCTION = 1;
const int ACTUAL_RESOURCE_COUNT = 7;
const int ALLOWED_ROAMING_HEROES = 8;
//implementation-dependent
extern const float SAFE_ATTACK_CONSTANT;
extern const int GOLD_RESERVE;
extern thread_local CCallback * cb;
@ -110,13 +103,6 @@ public:
const CGHeroInstance * get(bool doWeExpectNull = false) const;
const CGHeroInstance * get(const CPlayerSpecificInfoCallback * cb, bool doWeExpectNull = false) const;
bool validAndSet() const;
template<typename Handler> void serialize(Handler & handler)
{
handler & h;
handler & hid;
}
};
enum BattleState
@ -141,12 +127,6 @@ struct ObjectIdRef
ObjectIdRef(const CGObjectInstance * obj);
bool operator<(const ObjectIdRef & rhs) const;
template<typename Handler> void serialize(Handler & h)
{
h & id;
}
};
template<Obj::Type id>
@ -228,8 +208,8 @@ bool isBlockVisitObj(const int3 & pos);
bool isWeeklyRevisitable(const Nullkiller * ai, const CGObjectInstance * obj);
bool isObjectRemovable(const CGObjectInstance * obj); //FIXME FIXME: move logic to object property!
bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength);
bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet *, uint64_t dangerStrength);
bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength, float safeAttackRatio);
bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet *, uint64_t dangerStrength, float safeAttackRatio);
bool compareHeroStrength(const CGHeroInstance * h1, const CGHeroInstance * h2);
bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2);

View File

@ -13,6 +13,7 @@
#include "../Engine/Nullkiller.h"
#include "../../../CCallback.h"
#include "../../../lib/mapObjects/MapObjects.h"
#include "../../../lib/IGameSettings.h"
#include "../../../lib/GameConstants.h"
namespace NKAI
@ -90,7 +91,7 @@ std::vector<SlotInfo> ArmyManager::getSortedSlots(const CCreatureSet * target, c
{
for(auto & i : armyPtr->Slots())
{
auto cre = dynamic_cast<const CCreature*>(i.second->type);
auto cre = dynamic_cast<const CCreature*>(i.second->getType());
auto & slotInfp = creToPower[cre];
slotInfp.creature = cre;
@ -144,7 +145,7 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
for(auto & slot : sortedSlots)
{
alignmentMap[slot.creature->getFaction()] += slot.power;
alignmentMap[slot.creature->getFactionID()] += slot.power;
}
std::set<FactionID> allowedFactions;
@ -152,16 +153,6 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
uint64_t armyValue = 0;
TemporaryArmy newArmyInstance;
auto bonusModifiers = armyCarrier->getBonuses(Selector::type()(BonusType::MORALE));
for(auto bonus : *bonusModifiers)
{
// army bonuses will change and object bonuses are temporary
if(bonus->source != BonusSource::ARMY && bonus->source != BonusSource::OBJECT_INSTANCE && bonus->source != BonusSource::OBJECT_TYPE)
{
newArmyInstance.addNewBonus(std::make_shared<Bonus>(*bonus));
}
}
while(allowedFactions.size() < alignmentMap.size())
{
@ -178,7 +169,7 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
for(auto & slot : sortedSlots)
{
if(vstd::contains(allowedFactions, slot.creature->getFaction()))
if(vstd::contains(allowedFactions, slot.creature->getFactionID()))
{
auto slotID = newArmyInstance.getSlotFor(slot.creature->getId());
@ -197,16 +188,18 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
auto morale = slot.second->moraleVal();
auto multiplier = 1.0f;
const float BadMoraleChance = 0.083f;
const float HighMoraleChance = 0.04f;
const auto & badMoraleDice = cb->getSettings().getVector(EGameSettings::COMBAT_BAD_MORALE_DICE);
const auto & highMoraleDice = cb->getSettings().getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE);
if(morale < 0)
if(morale < 0 && !badMoraleDice.empty())
{
multiplier += morale * BadMoraleChance;
size_t diceIndex = std::min<size_t>(badMoraleDice.size(), -morale) - 1;
multiplier -= 1.0 / badMoraleDice.at(diceIndex);
}
else if(morale > 0)
else if(morale > 0 && !highMoraleDice.empty())
{
multiplier += morale * HighMoraleChance;
size_t diceIndex = std::min<size_t>(highMoraleDice.size(), morale) - 1;
multiplier += 1.0 / highMoraleDice.at(diceIndex);
}
newValue += multiplier * slot.second->getPower();
@ -316,6 +309,8 @@ std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
? dynamic_cast<const CGTownInstance *>(dwelling)
: nullptr;
std::set<SlotID> alreadyDisbanded;
for(int i = dwelling->creatures.size() - 1; i >= 0; i--)
{
auto ci = infoFromDC(dwelling->creatures[i]);
@ -329,18 +324,71 @@ std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
if(!ci.count) continue;
// Calculate the market value of the new stack
TResources newStackValue = ci.creID.toCreature()->getFullRecruitCost() * ci.count;
SlotID dst = hero->getSlotFor(ci.creID);
// Keep track of the least valuable slot in the hero's army
SlotID leastValuableSlot;
TResources leastValuableStackValue;
leastValuableStackValue[6] = std::numeric_limits<int>::max();
bool shouldDisband = false;
if(!hero->hasStackAtSlot(dst)) //need another new slot for this stack
{
if(!freeHeroSlots) //no more place for stacks
continue;
if(!freeHeroSlots) // No free slots; consider replacing
{
// Check for the least valuable existing stack
for (auto& slot : hero->Slots())
{
if (alreadyDisbanded.find(slot.first) != alreadyDisbanded.end())
continue;
if(slot.second->getCreatureID() != CreatureID::NONE)
{
TResources currentStackValue = slot.second->getCreatureID().toCreature()->getFullRecruitCost() * slot.second->getCount();
if (town && slot.second->getCreatureID().toCreature()->getFactionID() == town->getFactionID())
continue;
if(currentStackValue.marketValue() < leastValuableStackValue.marketValue())
{
leastValuableStackValue = currentStackValue;
leastValuableSlot = slot.first;
}
}
}
// Decide whether to replace the least valuable stack
if(newStackValue.marketValue() <= leastValuableStackValue.marketValue())
{
continue; // Skip if the new stack isn't worth replacing
}
else
{
shouldDisband = true;
}
}
else
{
freeHeroSlots--; //new slot will be occupied
}
}
vstd::amin(ci.count, availableRes / ci.creID.toCreature()->getFullRecruitCost()); //max count we can afford
if(!ci.count) continue;
int disbandMalus = 0;
if (shouldDisband)
{
disbandMalus = leastValuableStackValue / ci.creID.toCreature()->getFullRecruitCost();
alreadyDisbanded.insert(leastValuableSlot);
}
ci.count -= disbandMalus;
if(ci.count <= 0)
continue;
ci.level = i; //this is important for Dungeon Summoning Portal
creaturesInDwellings.push_back(ci);

View File

@ -14,8 +14,6 @@
#include "../../../lib/GameConstants.h"
#include "../../../lib/VCMI_Lib.h"
#include "../../../lib/CTownHandler.h"
#include "../../../lib/CBuildingHandler.h"
namespace NKAI
{

View File

@ -10,13 +10,14 @@
#include "../StdInc.h"
#include "../Engine/Nullkiller.h"
#include "../Engine/Nullkiller.h"
#include "../../../lib/entities/building/CBuilding.h"
namespace NKAI
{
void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo)
{
auto townInfo = developmentInfo.town->town;
auto townInfo = developmentInfo.town->getTown();
auto creatures = townInfo->creatures;
auto buildings = townInfo->getAllBuildings();
@ -30,17 +31,14 @@ void BuildAnalyzer::updateTownDwellings(TownDevelopmentInfo & developmentInfo)
}
}
BuildingID prefixes[] = {BuildingID::DWELL_UP_FIRST, BuildingID::DWELL_FIRST};
for(int level = 0; level < GameConstants::CREATURES_PER_TOWN; level++)
for(int level = 0; level < developmentInfo.town->getTown()->creatures.size(); level++)
{
logAi->trace("Checking dwelling level %d", level);
BuildingInfo nextToBuild = BuildingInfo();
for(BuildingID prefix : prefixes)
for(int upgradeIndex : {1, 0})
{
BuildingID building = BuildingID(prefix + level);
BuildingID building = BuildingID(BuildingID::getDwellingFromLevel(level, upgradeIndex));
if(!vstd::contains(buildings, building))
continue; // no such building in town
@ -74,16 +72,23 @@ void BuildAnalyzer::updateOtherBuildings(TownDevelopmentInfo & developmentInfo)
if(developmentInfo.existingDwellings.size() >= 2 && ai->cb->getDate(Date::DAY_OF_WEEK) > boost::date_time::Friday)
{
otherBuildings.push_back({BuildingID::CITADEL, BuildingID::CASTLE});
otherBuildings.push_back({BuildingID::HORDE_1});
otherBuildings.push_back({BuildingID::HORDE_2});
}
otherBuildings.push_back({ BuildingID::CITADEL, BuildingID::CASTLE });
otherBuildings.push_back({ BuildingID::RESOURCE_SILO });
otherBuildings.push_back({ BuildingID::SPECIAL_1 });
otherBuildings.push_back({ BuildingID::SPECIAL_2 });
otherBuildings.push_back({ BuildingID::SPECIAL_3 });
otherBuildings.push_back({ BuildingID::SPECIAL_4 });
otherBuildings.push_back({ BuildingID::MARKETPLACE });
for(auto & buildingSet : otherBuildings)
{
for(auto & buildingID : buildingSet)
{
if(!developmentInfo.town->hasBuilt(buildingID) && developmentInfo.town->town->buildings.count(buildingID))
if(!developmentInfo.town->hasBuilt(buildingID) && developmentInfo.town->getTown()->buildings.count(buildingID))
{
developmentInfo.addBuildingToBuild(getBuildingOrPrerequisite(developmentInfo.town, buildingID));
@ -100,10 +105,17 @@ int32_t convertToGold(const TResources & res)
+ 125 * (res[EGameResID::GEMS] + res[EGameResID::CRYSTAL] + res[EGameResID::MERCURY] + res[EGameResID::SULFUR]);
}
TResources withoutGold(TResources other)
{
other[GameResID::GOLD] = 0;
return other;
}
TResources BuildAnalyzer::getResourcesRequiredNow() const
{
auto resourcesAvailable = ai->getFreeResources();
auto result = requiredResources - resourcesAvailable;
auto result = withoutGold(armyCost) + requiredResources - resourcesAvailable;
result.positive();
@ -113,7 +125,7 @@ TResources BuildAnalyzer::getResourcesRequiredNow() const
TResources BuildAnalyzer::getTotalResourcesRequired() const
{
auto resourcesAvailable = ai->getFreeResources();
auto result = totalDevelopmentCost - resourcesAvailable;
auto result = totalDevelopmentCost + withoutGold(armyCost) - resourcesAvailable;
result.positive();
@ -135,6 +147,8 @@ void BuildAnalyzer::update()
auto towns = ai->cb->getTownsInfo();
float economyDevelopmentCost = 0;
for(const CGTownInstance* town : towns)
{
logAi->trace("Checking town %s", town->getNameTranslated());
@ -147,6 +161,11 @@ void BuildAnalyzer::update()
requiredResources += developmentInfo.requiredResources;
totalDevelopmentCost += developmentInfo.townDevelopmentCost;
for(auto building : developmentInfo.toBuild)
{
if (building.dailyIncome[EGameResID::GOLD] > 0)
economyDevelopmentCost += building.buildCostWithPrerequisites[EGameResID::GOLD];
}
armyCost += developmentInfo.armyCost;
for(auto bi : developmentInfo.toBuild)
@ -165,15 +184,7 @@ void BuildAnalyzer::update()
updateDailyIncome();
if(ai->cb->getDate(Date::DAY) == 1)
{
goldPressure = 1;
}
else
{
goldPressure = ai->getLockedResources()[EGameResID::GOLD] / 5000.0f
+ (float)armyCost[EGameResID::GOLD] / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f);
}
goldPressure = (ai->getLockedResources()[EGameResID::GOLD] + (float)armyCost[EGameResID::GOLD] + economyDevelopmentCost) / (1 + 2 * ai->getFreeGold() + (float)dailyIncome[EGameResID::GOLD] * 7.0f);
logAi->trace("Gold pressure: %f", goldPressure);
}
@ -192,7 +203,7 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
bool excludeDwellingDependencies) const
{
BuildingID building = toBuild;
auto townInfo = town->town;
auto townInfo = town->getTown();
const CBuilding * buildPtr = townInfo->buildings.at(building);
const CCreature * creature = nullptr;
@ -203,8 +214,8 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
if(BuildingID::DWELL_FIRST <= toBuild && toBuild <= BuildingID::DWELL_UP_LAST)
{
creatureLevel = (toBuild - BuildingID::DWELL_FIRST) % GameConstants::CREATURES_PER_TOWN;
creatureUpgrade = (toBuild - BuildingID::DWELL_FIRST) / GameConstants::CREATURES_PER_TOWN;
creatureLevel = BuildingID::getLevelFromDwelling(toBuild);
creatureUpgrade = BuildingID::getUpgradedFromDwelling(toBuild);
}
else if(toBuild == BuildingID::HORDE_1 || toBuild == BuildingID::HORDE_1_UPGR)
{
@ -231,6 +242,12 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
logAi->trace("checking %s", info.name);
logAi->trace("buildInfo %s", info.toString());
int highestFort = 0;
for (auto twn : ai->cb->getTownsInfo())
{
highestFort = std::max(highestFort, (int)twn->fortLevel());
}
if(!town->hasBuilt(building))
{
auto canBuild = ai->cb->canBuildStructure(town, building);
@ -267,7 +284,7 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
BuildingInfo prerequisite = getBuildingOrPrerequisite(town, missingBuildings[0], excludeDwellingDependencies);
prerequisite.buildCostWithPrerequisits += info.buildCost;
prerequisite.buildCostWithPrerequisites += info.buildCost;
prerequisite.creatureCost = info.creatureCost;
prerequisite.creatureGrows = info.creatureGrows;
prerequisite.creatureLevel = info.creatureLevel;
@ -275,7 +292,15 @@ BuildingInfo BuildAnalyzer::getBuildingOrPrerequisite(
prerequisite.baseCreatureID = info.baseCreatureID;
prerequisite.prerequisitesCount++;
prerequisite.armyCost = info.armyCost;
prerequisite.dailyIncome = info.dailyIncome;
bool haveSameOrBetterFort = false;
if (prerequisite.id == BuildingID::FORT && highestFort >= CGTownInstance::EFortLevel::FORT)
haveSameOrBetterFort = true;
if (prerequisite.id == BuildingID::CITADEL && highestFort >= CGTownInstance::EFortLevel::CITADEL)
haveSameOrBetterFort = true;
if (prerequisite.id == BuildingID::CASTLE && highestFort >= CGTownInstance::EFortLevel::CASTLE)
haveSameOrBetterFort = true;
if(!haveSameOrBetterFort)
prerequisite.dailyIncome = info.dailyIncome;
return prerequisite;
}
@ -308,9 +333,7 @@ void BuildAnalyzer::updateDailyIncome()
const CGMine* mine = dynamic_cast<const CGMine*>(obj);
if(mine)
{
dailyIncome[mine->producedResource.getNum()] += mine->producedQuantity;
}
dailyIncome += mine->dailyIncome();
}
for(const CGTownInstance* town : towns)
@ -323,7 +346,7 @@ bool BuildAnalyzer::hasAnyBuilding(int32_t alignment, BuildingID bid) const
{
for(auto tdi : developmentInfos)
{
if(tdi.town->getFaction() == alignment && tdi.town->hasBuilt(bid))
if(tdi.town->getFactionID() == alignment && tdi.town->hasBuilt(bid))
return true;
}
@ -340,7 +363,8 @@ void TownDevelopmentInfo::addExistingDwelling(const BuildingInfo & existingDwell
void TownDevelopmentInfo::addBuildingToBuild(const BuildingInfo & nextToBuild)
{
townDevelopmentCost += nextToBuild.buildCostWithPrerequisits;
townDevelopmentCost += nextToBuild.buildCostWithPrerequisites;
townDevelopmentCost += withoutGold(nextToBuild.armyCost);
if(nextToBuild.canBuild)
{
@ -361,7 +385,7 @@ BuildingInfo::BuildingInfo()
creatureGrows = 0;
creatureID = CreatureID::NONE;
buildCost = 0;
buildCostWithPrerequisits = 0;
buildCostWithPrerequisites = 0;
prerequisitesCount = 0;
name.clear();
armyStrength = 0;
@ -376,7 +400,7 @@ BuildingInfo::BuildingInfo(
{
id = building->bid;
buildCost = building->resources;
buildCostWithPrerequisits = building->resources;
buildCostWithPrerequisites = building->resources;
dailyIncome = building->produce;
exists = town->hasBuilt(id);
prerequisitesCount = 1;

View File

@ -22,7 +22,7 @@ class DLL_EXPORT BuildingInfo
public:
BuildingID id;
TResources buildCost;
TResources buildCostWithPrerequisits;
TResources buildCostWithPrerequisites;
int creatureGrows;
uint8_t creatureLevel;
TResources creatureCost;

View File

@ -13,6 +13,7 @@
#include "../Engine/Nullkiller.h"
#include "../pforeach.h"
#include "../../../lib/CRandomGenerator.h"
#include "../../../lib/logging/VisualLogger.h"
namespace NKAI
{
@ -24,6 +25,41 @@ double HitMapInfo::value() const
return danger / std::sqrt(turn / 3.0f + 1);
}
void logHitmap(PlayerColor playerID, DangerHitMapAnalyzer & data)
{
#if NKAI_TRACE_LEVEL >= 1
logVisual->updateWithLock(playerID.toString() + ".danger.max", [&data](IVisualLogBuilder & b)
{
foreach_tile_pos([&b, &data](const int3 & pos)
{
auto & treat = data.getTileThreat(pos).maximumDanger;
b.addText(pos, std::to_string(treat.danger));
if(treat.hero.validAndSet())
{
b.addText(pos, std::to_string(treat.turn));
b.addText(pos, treat.hero->getNameTranslated());
}
});
});
logVisual->updateWithLock(playerID.toString() + ".danger.fast", [&data](IVisualLogBuilder & b)
{
foreach_tile_pos([&b, &data](const int3 & pos)
{
auto & treat = data.getTileThreat(pos).fastestDanger;
b.addText(pos, std::to_string(treat.danger));
if(treat.hero.validAndSet())
{
b.addText(pos, std::to_string(treat.turn));
b.addText(pos, treat.hero->getNameTranslated());
}
});
});
#endif
}
void DangerHitMapAnalyzer::updateHitMap()
{
if(hitMapUpToDate)
@ -53,6 +89,13 @@ void DangerHitMapAnalyzer::updateHitMap()
heroes[hero->tempOwner][hero] = HeroRole::MAIN;
}
if(obj->ID == Obj::TOWN)
{
auto town = dynamic_cast<const CGTownInstance *>(obj);
if(town->garrisonHero)
heroes[town->garrisonHero->tempOwner][town->garrisonHero] = HeroRole::MAIN;
}
}
auto ourTowns = cb->getTownsInfo();
@ -96,6 +139,7 @@ void DangerHitMapAnalyzer::updateHitMap()
newThreat.hero = path.targetHero;
newThreat.turn = path.turn();
newThreat.threat = path.getHeroStrength() * (1 - path.movementCost() / 2.0);
newThreat.danger = path.getHeroStrength();
if(newThreat.value() > node.maximumDanger.value())
@ -144,6 +188,8 @@ void DangerHitMapAnalyzer::updateHitMap()
}
logAi->trace("Danger hit map updated in %ld", timeElapsed(start));
logHitmap(ai->playerID, *this);
}
void DangerHitMapAnalyzer::calculateTileOwners()
@ -270,8 +316,8 @@ uint64_t DangerHitMapAnalyzer::enemyCanKillOurHeroesAlongThePath(const AIPath &
const auto& info = getTileThreat(tile);
return (info.fastestDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.fastestDanger.danger))
|| (info.maximumDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.maximumDanger.danger));
return (info.fastestDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.fastestDanger.danger, ai->settings->getSafeAttackRatio()))
|| (info.maximumDanger.turn <= turn && !isSafeToVisit(path.targetHero, path.heroArmy, info.maximumDanger.danger, ai->settings->getSafeAttackRatio()));
}
const HitMapNode & DangerHitMapAnalyzer::getObjectThreat(const CGObjectInstance * obj) const
@ -302,6 +348,7 @@ std::set<const CGObjectInstance *> DangerHitMapAnalyzer::getOneTurnAccessibleObj
void DangerHitMapAnalyzer::reset()
{
hitMapUpToDate = false;
tileOwnersUpToDate = false;
}
}

View File

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

View File

@ -11,8 +11,7 @@
#include "../StdInc.h"
#include "../Engine/Nullkiller.h"
#include "../../../lib/mapObjects/MapObjects.h"
#include "../../../lib/CHeroHandler.h"
#include "../../../lib/GameSettings.h"
#include "../../../lib/IGameSettings.h"
namespace NKAI
{
@ -71,7 +70,7 @@ float HeroManager::evaluateSecSkill(SecondarySkill skill, const CGHeroInstance *
float HeroManager::evaluateSpeciality(const CGHeroInstance * hero) const
{
auto heroSpecial = Selector::source(BonusSource::HERO_SPECIAL, BonusSourceID(hero->type->getId()));
auto heroSpecial = Selector::source(BonusSource::HERO_SPECIAL, BonusSourceID(hero->getHeroTypeID()));
auto secondarySkillBonus = Selector::targetSourceType()(BonusSource::SECONDARY_SKILL);
auto specialSecondarySkillBonuses = hero->getBonuses(heroSpecial.And(secondarySkillBonus));
auto secondarySkillBonuses = hero->getBonuses(Selector::sourceTypeSel(BonusSource::SECONDARY_SKILL));
@ -96,7 +95,7 @@ float HeroManager::evaluateSpeciality(const CGHeroInstance * hero) const
float HeroManager::evaluateFightingStrength(const CGHeroInstance * hero) const
{
return evaluateSpeciality(hero) + wariorSkillsScores.evaluateSecSkills(hero) + hero->level * 1.5f;
return evaluateSpeciality(hero) + wariorSkillsScores.evaluateSecSkills(hero) + hero->getBasePrimarySkillValue(PrimarySkill::ATTACK) + hero->getBasePrimarySkillValue(PrimarySkill::DEFENSE) + hero->getBasePrimarySkillValue(PrimarySkill::SPELL_POWER) + hero->getBasePrimarySkillValue(PrimarySkill::KNOWLEDGE);
}
void HeroManager::update()
@ -109,7 +108,7 @@ void HeroManager::update()
for(auto & hero : myHeroes)
{
scores[hero] = evaluateFightingStrength(hero);
knownFightingStrength[hero->id] = hero->getFightingStrength();
knownFightingStrength[hero->id] = hero->getHeroStrength();
}
auto scoreSort = [&](const CGHeroInstance * h1, const CGHeroInstance * h2) -> bool
@ -148,7 +147,10 @@ void HeroManager::update()
HeroRole HeroManager::getHeroRole(const HeroPtr & hero) const
{
return heroRoles.at(hero);
if (heroRoles.find(hero) != heroRoles.end())
return heroRoles.at(hero);
else
return HeroRole::SCOUT;
}
const std::map<HeroPtr, HeroRole> & HeroManager::getHeroRoles() const
@ -189,15 +191,13 @@ float HeroManager::evaluateHero(const CGHeroInstance * hero) const
return evaluateFightingStrength(hero);
}
bool HeroManager::heroCapReached() const
bool HeroManager::heroCapReached(bool includeGarrisoned) const
{
const bool includeGarnisoned = true;
int heroCount = cb->getHeroCount(ai->playerID, includeGarnisoned);
int heroCount = cb->getHeroCount(ai->playerID, includeGarrisoned);
return heroCount >= ALLOWED_ROAMING_HEROES
|| heroCount >= ai->settings->getMaxRoamingHeroes()
|| heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)
|| heroCount >= VLC->settings()->getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP);
return heroCount >= ai->settings->getMaxRoamingHeroes()
|| heroCount >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_ON_MAP_CAP)
|| heroCount >= cb->getSettings().getInteger(EGameSettings::HEROES_PER_PLAYER_TOTAL_CAP);
}
float HeroManager::getFightingStrengthCached(const CGHeroInstance * hero) const
@ -205,7 +205,7 @@ float HeroManager::getFightingStrengthCached(const CGHeroInstance * hero) const
auto cached = knownFightingStrength.find(hero->id);
//FIXME: fallback to hero->getFightingStrength() is VERY slow on higher difficulties (no object graph? map reveal?)
return cached != knownFightingStrength.end() ? cached->second : hero->getFightingStrength();
return cached != knownFightingStrength.end() ? cached->second : hero->getHeroStrength();
}
float HeroManager::getMagicStrength(const CGHeroInstance * hero) const
@ -282,7 +282,7 @@ const CGHeroInstance * HeroManager::findHeroWithGrail() const
return nullptr;
}
const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) const
const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit, const CGTownInstance* townToSpare) const
{
const CGHeroInstance * weakestHero = nullptr;
auto myHeroes = ai->cb->getHeroesInfo();
@ -293,12 +293,13 @@ const CGHeroInstance * HeroManager::findWeakHeroToDismiss(uint64_t armyLimit) co
|| existingHero->getArmyStrength() >armyLimit
|| getHeroRole(existingHero) == HeroRole::MAIN
|| existingHero->movementPointsRemaining()
|| (townToSpare != nullptr && existingHero->visitedTown == townToSpare)
|| existingHero->artifactsWorn.size() > (existingHero->hasSpellbook() ? 2 : 1))
{
continue;
}
if(!weakestHero || weakestHero->getFightingStrength() > existingHero->getFightingStrength())
if(!weakestHero || weakestHero->getHeroStrength() > existingHero->getHeroStrength())
{
weakestHero = existingHero;
}

View File

@ -14,8 +14,6 @@
#include "../../../lib/GameConstants.h"
#include "../../../lib/VCMI_Lib.h"
#include "../../../lib/CTownHandler.h"
#include "../../../lib/CBuildingHandler.h"
namespace NKAI
{
@ -58,9 +56,9 @@ public:
float evaluateSecSkill(SecondarySkill skill, const CGHeroInstance * hero) const;
float evaluateHero(const CGHeroInstance * hero) const;
bool canRecruitHero(const CGTownInstance * t = nullptr) const;
bool heroCapReached() const;
bool heroCapReached(bool includeGarrisoned = true) const;
const CGHeroInstance * findHeroWithGrail() const;
const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit) const;
const CGHeroInstance * findWeakHeroToDismiss(uint64_t armyLimit, const CGTownInstance * townToSpare = nullptr) const;
float getMagicStrength(const CGHeroInstance * hero) const;
float getFightingStrengthCached(const CGHeroInstance * hero) const;

View File

@ -97,9 +97,10 @@ std::optional<const CGObjectInstance *> ObjectClusterizer::getBlocker(const AIPa
{
auto guardPos = ai->cb->getGuardingCreaturePosition(node.coord);
blockers = ai->cb->getVisitableObjs(node.coord);
if (ai->cb->isVisible(node.coord))
blockers = ai->cb->getVisitableObjs(node.coord);
if(guardPos.valid())
if(guardPos.valid() && ai->cb->isVisible(guardPos))
{
auto guard = ai->cb->getTopObj(ai->cb->getGuardingCreaturePosition(node.coord));
@ -146,6 +147,13 @@ std::optional<const CGObjectInstance *> ObjectClusterizer::getBlocker(const AIPa
return blocker;
}
auto danger = ai->dangerEvaluator->evaluateDanger(blocker);
if(danger > 0 && blocker->isBlockedVisitable() && isObjectRemovable(blocker))
{
return blocker;
}
return std::optional< const CGObjectInstance *>();
}
@ -467,9 +475,11 @@ void ObjectClusterizer::clusterizeObject(
heroesProcessed.insert(path.targetHero);
float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)));
float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), PriorityEvaluator::PriorityTier::HUNTER_GATHER);
if(priority < MIN_PRIORITY)
if(ai->settings->isUseFuzzy() && priority < MIN_PRIORITY)
continue;
else if (priority <= 0)
continue;
ClusterMap::accessor cluster;
@ -488,12 +498,14 @@ void ObjectClusterizer::clusterizeObject(
heroesProcessed.insert(path.targetHero);
float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)));
float priority = priorityEvaluator->evaluate(Goals::sptr(Goals::ExecuteHeroChain(path, obj)), PriorityEvaluator::PriorityTier::HUNTER_GATHER);
if(priority < MIN_PRIORITY)
if (ai->settings->isUseFuzzy() && priority < MIN_PRIORITY)
continue;
else if (priority <= 0)
continue;
bool interestingObject = path.turn() <= 2 || priority > 0.5f;
bool interestingObject = path.turn() <= 2 || priority > (ai->settings->isUseFuzzy() ? 0.5f : 0);
if(interestingObject)
{

View File

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

View File

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

View File

@ -68,14 +68,6 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(
logAi->trace("Path found %s", path.toString());
#endif
if(nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.getHeroStrength());
#endif
continue;
}
if(objToVisit && !force && !shouldVisit(nullkiller, path.targetHero, objToVisit))
{
#if NKAI_TRACE_LEVEL >= 2
@ -87,6 +79,9 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(
auto hero = path.targetHero;
auto danger = path.getTotalDanger();
if (hero->getOwner() != nullkiller->playerID)
continue;
if(nullkiller->heroManager->getHeroRole(hero) == HeroRole::SCOUT
&& (path.getTotalDanger() == 0 || path.turn() > 0)
&& path.exchangeCount > 1)
@ -119,7 +114,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(
continue;
}
auto isSafe = isSafeToVisit(hero, path.heroArmy, danger);
auto isSafe = isSafeToVisit(hero, path.heroArmy, danger, nullkiller->settings->getSafeAttackRatio());
#if NKAI_TRACE_LEVEL >= 2
logAi->trace(
@ -212,7 +207,7 @@ void CaptureObjectsBehavior::decomposeObjects(
vstd::concatenate(tasksLocal, getVisitGoals(paths, nullkiller, objToVisit, specificObjects));
}
std::lock_guard<std::mutex> lock(sync); // FIXME: consider using tbb::parallel_reduce instead to avoid mutex overhead
std::lock_guard lock(sync); // FIXME: consider using tbb::parallel_reduce instead to avoid mutex overhead
vstd::concatenate(result, tasksLocal);
});
}

View File

@ -130,7 +130,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa
tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5)));
return true;
return false;
}
else if(ai->heroManager->getHeroRole(town->garrisonHero.get()) == HeroRole::MAIN)
{
@ -141,7 +141,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa
{
tasks.push_back(Goals::sptr(Goals::DismissHero(heroToDismiss).setpriority(5)));
return true;
return false;
}
}
}
@ -158,11 +158,10 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
threats.push_back(threatNode.fastestDanger); // no guarantee that fastest danger will be there
if(town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks, ai))
if (town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks, ai))
{
return;
}
if(!threatNode.fastestDanger.hero)
{
logAi->trace("No threat found for town %s", town->getNameTranslated());
@ -240,7 +239,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
if(path.turn() <= threat.turn - 2)
{
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Defer defence of %s by %s because he has enough time to reach the town next trun",
logAi->trace("Defer defence of %s by %s because he has enough time to reach the town next turn",
town->getObjectName(),
path.targetHero->getObjectName());
#endif
@ -250,6 +249,16 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
continue;
}
if (!path.targetHero->canBeMergedWith(*town))
{
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Can't merge armies of hero %s and town %s",
path.targetHero->getObjectName(),
town->getObjectName());
#endif
continue;
}
if(path.targetHero == town->visitingHero.get() && path.exchangeCount == 1)
{
#if NKAI_TRACE_LEVEL >= 1
@ -261,6 +270,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
// dismiss creatures we are not able to pick to be able to hide in garrison
if(town->garrisonHero
|| town->getUpperArmy()->stacksCount() == 0
|| path.targetHero->canBeMergedWith(*town)
|| (town->getUpperArmy()->getArmyStrength() < 500 && town->fortLevel() >= CGTownInstance::CITADEL))
{
tasks.push_back(
@ -292,7 +302,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
continue;
}
if(threat.turn == 0 || (path.turn() <= threat.turn && path.getHeroStrength() * SAFE_ATTACK_CONSTANT >= threat.danger))
if(threat.turn == 0 || (path.turn() <= threat.turn && path.getHeroStrength() * ai->settings->getSafeAttackRatio() >= threat.danger))
{
if(ai->arePathHeroesLocked(path))
{
@ -343,23 +353,14 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
}
else if(town->visitingHero && path.targetHero != town->visitingHero && !path.containsHero(town->visitingHero))
{
if(town->garrisonHero)
if(town->garrisonHero && town->garrisonHero != path.targetHero)
{
if(ai->heroManager->getHeroRole(town->visitingHero.get()) == HeroRole::SCOUT
&& town->visitingHero->getArmyStrength() < path.heroArmy->getArmyStrength() / 20)
{
if(path.turn() == 0)
sequence.push_back(sptr(DismissHero(town->visitingHero.get())));
}
else
{
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Cancel moving %s to defend town %s as the town has garrison hero",
path.targetHero->getObjectName(),
town->getObjectName());
logAi->trace("Cancel moving %s to defend town %s as the town has garrison hero",
path.targetHero->getObjectName(),
town->getObjectName());
#endif
continue;
}
continue;
}
else if(path.turn() == 0)
{
@ -405,6 +406,9 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & threat, const CGTownInstance * town, const Nullkiller * ai) const
{
if (threat.turn > 0 || town->garrisonHero || town->visitingHero)
return;
if(town->hasBuilt(BuildingID::TAVERN)
&& ai->cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST)
{
@ -415,6 +419,21 @@ void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitM
if(hero->getTotalStrength() < threat.danger)
continue;
bool heroAlreadyHiredInOtherTown = false;
for (const auto& task : tasks)
{
if (auto recruitGoal = dynamic_cast<Goals::RecruitHero*>(task.get()))
{
if (recruitGoal->getHero() == hero)
{
heroAlreadyHiredInOtherTown = true;
break;
}
}
}
if (heroAlreadyHiredInOtherTown)
continue;
auto myHeroes = ai->cb->getHeroesInfo();
#if NKAI_TRACE_LEVEL >= 1
@ -451,7 +470,7 @@ void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitM
}
else if(ai->heroManager->heroCapReached())
{
heroToDismiss = ai->heroManager->findWeakHeroToDismiss(hero->getArmyStrength());
heroToDismiss = ai->heroManager->findWeakHeroToDismiss(hero->getArmyStrength(), town);
if(!heroToDismiss)
continue;

View File

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

View File

@ -81,6 +81,9 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con
logAi->trace("Path found %s, %s, %lld", path.toString(), path.targetHero->getObjectName(), path.heroArmy->getArmyStrength());
#endif
if (path.targetHero->getOwner() != ai->playerID)
continue;
if(path.containsHero(hero))
{
#if NKAI_TRACE_LEVEL >= 2
@ -89,14 +92,6 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con
continue;
}
if(path.turn() > 0 && ai->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength());
#endif
continue;
}
if(ai->arePathHeroesLocked(path))
{
#if NKAI_TRACE_LEVEL >= 2
@ -150,7 +145,7 @@ Goals::TGoalVec GatherArmyBehavior::deliverArmyToHero(const Nullkiller * ai, con
}
auto danger = path.getTotalDanger();
auto isSafe = isSafeToVisit(hero, path.heroArmy, danger);
auto isSafe = isSafeToVisit(hero, path.heroArmy, danger, ai->settings->getSafeAttackRatio());
#if NKAI_TRACE_LEVEL >= 2
logAi->trace(
@ -292,17 +287,6 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
continue;
}
auto heroRole = ai->heroManager->getHeroRole(path.targetHero);
if(heroRole == HeroRole::SCOUT
&& ai->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.heroArmy->getArmyStrength());
#endif
continue;
}
auto upgrade = ai->armyManager->calculateCreaturesUpgrade(path.heroArmy, upgrader, availableResources);
if(!upgrader->garrisonHero
@ -320,14 +304,6 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
armyToGetOrBuy.upgradeValue -= path.heroArmy->getArmyStrength();
armyToGetOrBuy.addArmyToBuy(
ai->armyManager->toSlotInfo(
ai->armyManager->getArmyAvailableToBuy(
path.heroArmy,
upgrader,
ai->getFreeResources(),
path.turn())));
upgrade.upgradeValue += armyToGetOrBuy.upgradeValue;
upgrade.upgradeCost += armyToGetOrBuy.upgradeCost;
vstd::concatenate(upgrade.resultingArmy, armyToGetOrBuy.resultingArmy);
@ -339,8 +315,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
{
for(auto hero : cb->getAvailableHeroes(upgrader))
{
auto scoutReinforcement = ai->armyManager->howManyReinforcementsCanBuy(hero, upgrader)
+ ai->armyManager->howManyReinforcementsCanGet(hero, upgrader);
auto scoutReinforcement = ai->armyManager->howManyReinforcementsCanGet(hero, upgrader);
if(scoutReinforcement >= armyToGetOrBuy.upgradeValue
&& ai->getFreeGold() >20000
@ -366,7 +341,7 @@ Goals::TGoalVec GatherArmyBehavior::upgradeArmy(const Nullkiller * ai, const CGT
auto danger = path.getTotalDanger();
auto isSafe = isSafeToVisit(path.targetHero, path.heroArmy, danger);
auto isSafe = isSafeToVisit(path.targetHero, path.heroArmy, danger, ai->settings->getSafeAttackRatio());
#if NKAI_TRACE_LEVEL >= 2
logAi->trace(

View File

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

View File

@ -79,6 +79,15 @@ bool needToRecruitHero(const Nullkiller * ai, const CGTownInstance * startupTown
bool isGoldPile = dynamic_cast<const CGResource *>(obj)
&& dynamic_cast<const CGResource *>(obj)->resourceID() == EGameResID::GOLD;
auto rewardable = dynamic_cast<const Rewardable::Interface *>(obj);
if(rewardable)
{
for(auto & info : rewardable->configuration.info)
if(info.reward.resources[EGameResID::GOLD] > 0)
isGoldPile = true;
}
if(isGoldPile
|| obj->ID == Obj::TREASURE_CHEST
|| obj->ID == Obj::CAMPFIRE

View File

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

View File

@ -8,6 +8,7 @@ set(Nullkiller_SRCS
Pathfinding/Actions/QuestAction.cpp
Pathfinding/Actions/BuyArmyAction.cpp
Pathfinding/Actions/BoatActions.cpp
Pathfinding/Actions/WhirlpoolAction.cpp
Pathfinding/Actions/TownPortalAction.cpp
Pathfinding/Actions/AdventureSpellCastMovementActions.cpp
Pathfinding/Rules/AILayerTransitionRule.cpp
@ -79,6 +80,7 @@ set(Nullkiller_HEADERS
Pathfinding/Actions/QuestAction.h
Pathfinding/Actions/BuyArmyAction.h
Pathfinding/Actions/BoatActions.h
Pathfinding/Actions/WhirlpoolAction.h
Pathfinding/Actions/TownPortalAction.h
Pathfinding/Actions/AdventureSpellCastMovementActions.h
Pathfinding/Rules/AILayerTransitionRule.h
@ -155,11 +157,7 @@ else()
endif()
target_include_directories(Nullkiller PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(Nullkiller PUBLIC vcmi fuzzylite::fuzzylite TBB::tbb)
target_link_libraries(Nullkiller PUBLIC vcmi fuzzylite::fuzzylite)
vcmi_set_output_dir(Nullkiller "AI")
enable_pch(Nullkiller)
if(APPLE_IOS AND NOT USING_CONAN)
install(IMPORTED_RUNTIME_ARTIFACTS TBB::tbb LIBRARY DESTINATION ${LIB_DIR}) # CMake 3.21+
endif()

View File

@ -17,8 +17,7 @@
namespace NKAI
{
#define MIN_AI_STRENGTH (0.5f) //lower when combat AI gets smarter
#define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us
constexpr float MIN_AI_STRENGTH = 0.5f; //lower when combat AI gets smarter
engineBase::engineBase()
{
@ -208,12 +207,6 @@ float TacticalAdvantageEngine::getTacticalAdvantage(const CArmedInstance * we, c
enemyFlyers->setValue(enemyStructure.flyers);
enemySpeed->setValue(enemyStructure.maxSpeed);
bool bank = dynamic_cast<const CBank *>(enemy);
if(bank)
bankPresent->setValue(1);
else
bankPresent->setValue(0);
const CGTownInstance * fort = dynamic_cast<const CGTownInstance *>(enemy);
if(fort)
castleWalls->setValue(fort->fortLevel());

View File

@ -20,25 +20,6 @@
namespace NKAI
{
ui64 FuzzyHelper::estimateBankDanger(const CBank * bank)
{
//this one is not fuzzy anymore, just calculate weighted average
auto objectInfo = bank->getObjectHandler()->getObjectInfo(bank->appearance);
CBankInfo * bankInfo = dynamic_cast<CBankInfo *>(objectInfo.get());
ui64 totalStrength = 0;
ui8 totalChance = 0;
for(auto config : bankInfo->getPossibleGuards(bank->cb))
{
totalStrength += config.second.totalStrength * config.first;
totalChance += config.first;
}
return totalStrength / std::max<ui8>(totalChance, 1); //avoid division by zero
}
ui64 FuzzyHelper::evaluateDanger(const int3 & tile, const CGHeroInstance * visitor, bool checkGuards)
{
auto cb = ai->cb.get();
@ -71,6 +52,15 @@ ui64 FuzzyHelper::evaluateDanger(const int3 & tile, const CGHeroInstance * visit
{
objectDanger += evaluateDanger(hero->visitedTown.get());
}
objectDanger *= ai->heroManager->getFightingStrengthCached(hero);
}
if (objWithID<Obj::TOWN>(dangerousObject))
{
auto town = dynamic_cast<const CGTownInstance*>(dangerousObject);
auto hero = town->garrisonHero;
if (hero)
objectDanger *= ai->heroManager->getFightingStrengthCached(hero);
}
if(objectDanger)
@ -136,10 +126,10 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj)
{
auto fortLevel = town->fortLevel();
if(fortLevel == CGTownInstance::EFortLevel::CASTLE)
danger += 10000;
if (fortLevel == CGTownInstance::EFortLevel::CASTLE)
danger = std::max(danger * 2, danger + 10000);
else if(fortLevel == CGTownInstance::EFortLevel::CITADEL)
danger += 4000;
danger = std::max(ui64(danger * 1.4), danger + 4000);
}
return danger;
@ -158,30 +148,14 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj)
return 0;
[[fallthrough]];
}
case Obj::MONSTER:
case Obj::GARRISON:
case Obj::GARRISON2:
case Obj::CREATURE_GENERATOR1:
case Obj::CREATURE_GENERATOR4:
case Obj::MINE:
case Obj::ABANDONED_MINE:
case Obj::PANDORAS_BOX:
case Obj::CRYPT: //crypt
case Obj::CREATURE_BANK: //crebank
case Obj::DRAGON_UTOPIA:
case Obj::SHIPWRECK: //shipwreck
case Obj::DERELICT_SHIP: //derelict ship
default:
{
const CArmedInstance * a = dynamic_cast<const CArmedInstance *>(obj);
return a->getArmyStrength();
if (a)
return a->getArmyStrength();
else
return 0;
}
case Obj::PYRAMID:
{
return estimateBankDanger(dynamic_cast<const CBank *>(obj));
}
default:
return 0;
}
}
}

View File

@ -10,12 +10,6 @@
#pragma once
#include "FuzzyEngines.h"
VCMI_LIB_NAMESPACE_BEGIN
class CBank;
VCMI_LIB_NAMESPACE_END
namespace NKAI
{
@ -30,8 +24,6 @@ private:
public:
FuzzyHelper(const Nullkiller * ai): ai(ai) {}
ui64 estimateBankDanger(const CBank * bank); //TODO: move to another class?
ui64 evaluateDanger(const CGObjectInstance * obj);
ui64 evaluateDanger(const int3 & tile, const CGHeroInstance * visitor, bool checkGuards = true);
};

View File

@ -34,13 +34,12 @@ using namespace Goals;
std::unique_ptr<ObjectGraph> Nullkiller::baseGraph;
Nullkiller::Nullkiller()
:activeHero(nullptr), scanDepth(ScanDepth::MAIN_FULL), useHeroChain(true)
: activeHero(nullptr)
, scanDepth(ScanDepth::MAIN_FULL)
, useHeroChain(true)
, memory(std::make_unique<AIMemory>())
{
memory = std::make_unique<AIMemory>();
settings = std::make_unique<Settings>();
useObjectGraph = settings->isObjectGraphAllowed();
openMap = settings->isOpenMap() || useObjectGraph;
}
bool canUseOpenMap(std::shared_ptr<CCallback> cb, PlayerColor playerID)
@ -62,17 +61,23 @@ bool canUseOpenMap(std::shared_ptr<CCallback> cb, PlayerColor playerID)
return false;
}
return cb->getStartInfo()->difficulty >= 3;
return true;
}
void Nullkiller::init(std::shared_ptr<CCallback> cb, AIGateway * gateway)
{
this->cb = cb;
this->gateway = gateway;
playerID = gateway->playerID;
this->playerID = gateway->playerID;
if(openMap && !canUseOpenMap(cb, playerID))
settings = std::make_unique<Settings>(cb->getStartInfo()->difficulty);
if(canUseOpenMap(cb, playerID))
{
useObjectGraph = settings->isObjectGraphAllowed();
openMap = settings->isOpenMap() || useObjectGraph;
}
else
{
useObjectGraph = false;
openMap = false;
@ -122,11 +127,14 @@ void TaskPlan::merge(TSubgoal task)
{
TGoalVec blockers;
if (task->asTask()->priority <= 0)
return;
for(auto & item : tasks)
{
for(auto objid : item.affectedObjects)
{
if(task == item.task || task->asTask()->isObjectAffected(objid))
if(task == item.task || task->asTask()->isObjectAffected(objid) || (task->asTask()->getHero() != nullptr && task->asTask()->getHero() == item.task->asTask()->getHero()))
{
if(item.task->asTask()->priority >= task->asTask()->priority)
return;
@ -166,20 +174,19 @@ Goals::TTask Nullkiller::choseBestTask(Goals::TGoalVec & tasks) const
return taskptr(*bestTask);
}
Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks) const
Goals::TTaskVec Nullkiller::buildPlan(TGoalVec & tasks, int priorityTier) const
{
TaskPlan taskPlan;
tbb::parallel_for(tbb::blocked_range<size_t>(0, tasks.size()), [this, &tasks](const tbb::blocked_range<size_t> & r)
tbb::parallel_for(tbb::blocked_range<size_t>(0, tasks.size()), [this, &tasks, priorityTier](const tbb::blocked_range<size_t> & r)
{
auto evaluator = this->priorityEvaluators->acquire();
for(size_t i = r.begin(); i != r.end(); i++)
{
auto task = tasks[i];
if(task->asTask()->priority <= 0)
task->asTask()->priority = evaluator->evaluate(task);
if (task->asTask()->priority <= 0 || priorityTier != PriorityEvaluator::PriorityTier::BUILDINGS)
task->asTask()->priority = evaluator->evaluate(task, priorityTier);
}
});
@ -216,7 +223,7 @@ void Nullkiller::decompose(Goals::TGoalVec & result, Goals::TSubgoal behavior, i
void Nullkiller::resetAiState()
{
std::unique_lock<std::mutex> lockGuard(aiStateMutex);
std::unique_lock lockGuard(aiStateMutex);
lockedResources = TResources();
scanDepth = ScanDepth::MAIN_FULL;
@ -236,7 +243,7 @@ void Nullkiller::updateAiState(int pass, bool fast)
{
boost::this_thread::interruption_point();
std::unique_lock<std::mutex> lockGuard(aiStateMutex);
std::unique_lock lockGuard(aiStateMutex);
auto start = std::chrono::high_resolution_clock::now();
@ -326,7 +333,7 @@ bool Nullkiller::arePathHeroesLocked(const AIPath & path) const
if(lockReason != HeroLockedReason::NOT_LOCKED)
{
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Hero %s is locked by STARTUP. Discarding %s", path.targetHero->getObjectName(), path.toString());
logAi->trace("Hero %s is locked by %d. Discarding %s", path.targetHero->getObjectName(), (int)lockReason, path.toString());
#endif
return true;
}
@ -347,12 +354,24 @@ void Nullkiller::makeTurn()
boost::lock_guard<boost::mutex> sharedStorageLock(AISharedStorage::locker);
const int MAX_DEPTH = 10;
const float FAST_TASK_MINIMAL_PRIORITY = 0.7f;
resetAiState();
Goals::TGoalVec bestTasks;
#if NKAI_TRACE_LEVEL >= 1
float totalHeroStrength = 0;
int totalTownLevel = 0;
for (auto heroInfo : cb->getHeroesInfo())
{
totalHeroStrength += heroInfo->getTotalStrength();
}
for (auto townInfo : cb->getTownsInfo())
{
totalTownLevel += townInfo->getTownLevel();
}
logAi->info("Beginning: Strength: %f Townlevel: %d Resources: %s", totalHeroStrength, totalTownLevel, cb->getResourceAmount().toString());
#endif
for(int i = 1; i <= settings->getMaxPass() && cb->getPlayerStatus(playerID) == EPlayerStatus::INGAME; i++)
{
auto start = std::chrono::high_resolution_clock::now();
@ -360,21 +379,30 @@ void Nullkiller::makeTurn()
Goals::TTask bestTask = taskptr(Goals::Invalid());
for(;i <= settings->getMaxPass(); i++)
while(true)
{
bestTasks.clear();
decompose(bestTasks, sptr(RecruitHeroBehavior()), 1);
decompose(bestTasks, sptr(BuyArmyBehavior()), 1);
decompose(bestTasks, sptr(BuildingBehavior()), 1);
bestTask = choseBestTask(bestTasks);
if(bestTask->priority >= FAST_TASK_MINIMAL_PRIORITY)
if(bestTask->priority > 0)
{
#if NKAI_TRACE_LEVEL >= 1
logAi->info("Pass %d: Performing prio 0 task %s with prio: %d", i, bestTask->toString(), bestTask->priority);
#endif
if(!executeTask(bestTask))
return;
updateAiState(i, true);
bool fastUpdate = true;
if (bestTask->getHero() != nullptr)
fastUpdate = false;
updateAiState(i, fastUpdate);
}
else
{
@ -382,7 +410,6 @@ void Nullkiller::makeTurn()
}
}
decompose(bestTasks, sptr(RecruitHeroBehavior()), 1);
decompose(bestTasks, sptr(CaptureObjectsBehavior()), 1);
decompose(bestTasks, sptr(ClusterBehavior()), MAX_DEPTH);
decompose(bestTasks, sptr(DefenceBehavior()), MAX_DEPTH);
@ -392,14 +419,26 @@ void Nullkiller::makeTurn()
if(!isOpenMap())
decompose(bestTasks, sptr(ExplorationBehavior()), MAX_DEPTH);
if(cb->getDate(Date::DAY) == 1 || heroManager->getHeroRoles().empty())
TTaskVec selectedTasks;
#if NKAI_TRACE_LEVEL >= 1
int prioOfTask = 0;
#endif
for (int prio = PriorityEvaluator::PriorityTier::INSTAKILL; prio <= PriorityEvaluator::PriorityTier::DEFEND; ++prio)
{
decompose(bestTasks, sptr(StartupBehavior()), 1);
#if NKAI_TRACE_LEVEL >= 1
prioOfTask = prio;
#endif
selectedTasks = buildPlan(bestTasks, prio);
if (!selectedTasks.empty() || settings->isUseFuzzy())
break;
}
auto selectedTasks = buildPlan(bestTasks);
std::sort(selectedTasks.begin(), selectedTasks.end(), [](const TTask& a, const TTask& b)
{
return a->priority > b->priority;
});
logAi->debug("Decission madel in %ld", timeElapsed(start));
logAi->debug("Decision madel in %ld", timeElapsed(start));
if(selectedTasks.empty())
{
@ -438,7 +477,7 @@ void Nullkiller::makeTurn()
bestTask->priority);
}
if(bestTask->priority < MIN_PRIORITY)
if((settings->isUseFuzzy() && bestTask->priority < MIN_PRIORITY) || (!settings->isUseFuzzy() && bestTask->priority <= 0))
{
auto heroes = cb->getHeroesInfo();
auto hasMp = vstd::contains_if(heroes, [](const CGHeroInstance * h) -> bool
@ -463,7 +502,9 @@ void Nullkiller::makeTurn()
continue;
}
#if NKAI_TRACE_LEVEL >= 1
logAi->info("Pass %d: Performing prio %d task %s with prio: %d", i, prioOfTask, bestTask->toString(), bestTask->priority);
#endif
if(!executeTask(bestTask))
{
if(hasAnySuccess)
@ -471,13 +512,27 @@ void Nullkiller::makeTurn()
else
return;
}
hasAnySuccess = true;
}
hasAnySuccess |= handleTrading();
if(!hasAnySuccess)
{
logAi->trace("Nothing was done this turn. Ending turn.");
#if NKAI_TRACE_LEVEL >= 1
totalHeroStrength = 0;
totalTownLevel = 0;
for (auto heroInfo : cb->getHeroesInfo())
{
totalHeroStrength += heroInfo->getTotalStrength();
}
for (auto townInfo : cb->getTownsInfo())
{
totalTownLevel += townInfo->getTownLevel();
}
logAi->info("End: Strength: %f Townlevel: %d Resources: %s", totalHeroStrength, totalTownLevel, cb->getResourceAmount().toString());
#endif
return;
}
@ -554,4 +609,102 @@ void Nullkiller::lockResources(const TResources & res)
lockedResources += res;
}
bool Nullkiller::handleTrading()
{
bool haveTraded = false;
bool shouldTryToTrade = true;
int marketId = -1;
for (auto town : cb->getTownsInfo())
{
if (town->hasBuiltSomeTradeBuilding())
{
marketId = town->id;
}
}
if (marketId == -1)
return false;
if (const CGObjectInstance* obj = cb->getObj(ObjectInstanceID(marketId), false))
{
if (const auto* m = dynamic_cast<const IMarket*>(obj))
{
while (shouldTryToTrade)
{
shouldTryToTrade = false;
buildAnalyzer->update();
TResources required = buildAnalyzer->getTotalResourcesRequired();
TResources income = buildAnalyzer->getDailyIncome();
TResources available = cb->getResourceAmount();
#if NKAI_TRACE_LEVEL >= 2
logAi->debug("Available %s", available.toString());
logAi->debug("Required %s", required.toString());
#endif
int mostWanted = -1;
int mostExpendable = -1;
float minRatio = std::numeric_limits<float>::max();
float maxRatio = std::numeric_limits<float>::min();
for (int i = 0; i < required.size(); ++i)
{
if (required[i] <= 0)
continue;
float ratio = static_cast<float>(available[i]) / required[i];
if (ratio < minRatio) {
minRatio = ratio;
mostWanted = i;
}
}
for (int i = 0; i < required.size(); ++i)
{
float ratio = available[i];
if (required[i] > 0)
ratio = static_cast<float>(available[i]) / required[i];
else
ratio = available[i];
bool okToSell = false;
if (i == GameResID::GOLD)
{
if (income[i] > 0 && !buildAnalyzer->isGoldPressureHigh())
okToSell = true;
}
else
{
if (required[i] <= 0 && income[i] > 0)
okToSell = true;
}
if (ratio > maxRatio && okToSell) {
maxRatio = ratio;
mostExpendable = i;
}
}
#if NKAI_TRACE_LEVEL >= 2
logAi->debug("mostExpendable: %d mostWanted: %d", mostExpendable, mostWanted);
#endif
if (mostExpendable == mostWanted || mostWanted == -1 || mostExpendable == -1)
return false;
int toGive;
int toGet;
m->getOffer(mostExpendable, mostWanted, toGive, toGet, EMarketMode::RESOURCE_RESOURCE);
//logAi->info("Offer is: I get %d of %s for %d of %s at %s", toGet, mostWanted, toGive, mostExpendable, obj->getObjectName());
//TODO trade only as much as needed
if (toGive && toGive <= available[mostExpendable]) //don't try to sell 0 resources
{
cb->trade(m->getObjInstanceID(), EMarketMode::RESOURCE_RESOURCE, GameResID(mostExpendable), GameResID(mostWanted), toGive);
#if NKAI_TRACE_LEVEL >= 1
logAi->info("Traded %d of %s for %d of %s at %s", toGive, mostExpendable, toGet, mostWanted, obj->getObjectName());
#endif
haveTraded = true;
shouldTryToTrade = true;
}
}
}
}
return haveTraded;
}
}

View File

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

View File

@ -15,6 +15,8 @@
#include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h"
#include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
#include "../../../lib/mapObjects/MapObjects.h"
#include "../../../lib/mapping/CMapDefines.h"
#include "../../../lib/RoadHandler.h"
#include "../../../lib/CCreatureHandler.h"
#include "../../../lib/VCMI_Lib.h"
#include "../../../lib/StartInfo.h"
@ -33,11 +35,9 @@
namespace NKAI
{
#define MIN_AI_STRENGHT (0.5f) //lower when combat AI gets smarter
#define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us
const float MIN_CRITICAL_VALUE = 2.0f;
constexpr float MIN_CRITICAL_VALUE = 2.0f;
EvaluationContext::EvaluationContext(const Nullkiller * ai)
EvaluationContext::EvaluationContext(const Nullkiller* ai)
: movementCost(0.0),
manaCost(0),
danger(0),
@ -51,9 +51,22 @@ EvaluationContext::EvaluationContext(const Nullkiller * ai)
heroRole(HeroRole::SCOUT),
turn(0),
strategicalValue(0),
conquestValue(0),
evaluator(ai),
enemyHeroDangerRatio(0),
armyGrowth(0)
threat(0),
armyGrowth(0),
armyInvolvement(0),
defenseValue(0),
isDefend(false),
threatTurns(INT_MAX),
involvesSailing(false),
isTradeBuilding(false),
isExchange(false),
isArmyUpgrade(false),
isHero(false),
isEnemy(false),
explorePriority(0)
{
}
@ -118,35 +131,17 @@ int32_t estimateTownIncome(CCallback * cb, const CGObjectInstance * target, cons
return booster * (town->hasFort() && town->tempOwner != PlayerColor::NEUTRAL ? booster * 500 : 250);
}
TResources getCreatureBankResources(const CGObjectInstance * target, const CGHeroInstance * hero)
int32_t getResourcesGoldReward(const TResources & res)
{
//Fixme: unused variable hero
int32_t result = 0;
auto objectInfo = target->getObjectHandler()->getObjectInfo(target->appearance);
CBankInfo * bankInfo = dynamic_cast<CBankInfo *>(objectInfo.get());
auto resources = bankInfo->getPossibleResourcesReward();
TResources result = TResources();
int sum = 0;
for(auto & reward : resources)
for(auto r : GameResID::ALL_RESOURCES())
{
result += reward.data * reward.chance;
sum += reward.chance;
if(res[r] > 0)
result += r == EGameResID::GOLD ? res[r] : res[r] * 100;
}
return sum > 1 ? result / sum : result;
}
uint64_t getResourcesGoldReward(const TResources & res)
{
int nonGoldResources = res[EGameResID::GEMS]
+ res[EGameResID::SULFUR]
+ res[EGameResID::WOOD]
+ res[EGameResID::ORE]
+ res[EGameResID::CRYSTAL]
+ res[EGameResID::MERCURY];
return res[EGameResID::GOLD] + 100 * nonGoldResources;
return result;
}
uint64_t getCreatureBankArmyReward(const CGObjectInstance * target, const CGHeroInstance * hero)
@ -173,10 +168,10 @@ uint64_t getCreatureBankArmyReward(const CGObjectInstance * target, const CGHero
for (auto c : creatures)
{
//Only if hero has slot for this creature in the army
auto ccre = dynamic_cast<const CCreature*>(c.data.type);
auto ccre = dynamic_cast<const CCreature*>(c.data.getType());
if (hero->getSlotFor(ccre).validSlot() || duplicatingSlots > 0)
{
result += (c.data.type->getAIValue() * c.data.count) * c.chance;
result += (c.data.getType()->getAIValue() * c.data.count) * c.chance;
}
/*else
{
@ -243,16 +238,16 @@ int getDwellingArmyCost(const CGObjectInstance * target)
auto creature = creLevel.second.back().toCreature();
auto creaturesAreFree = creature->getLevel() == 1;
if(!creaturesAreFree)
cost += creature->getRecruitCost(EGameResID::GOLD) * creLevel.first;
cost += creature->getFullRecruitCost().marketValue() * creLevel.first;
}
}
return cost;
}
static uint64_t evaluateArtifactArmyValue(const CArtifactInstance * art)
static uint64_t evaluateArtifactArmyValue(const CArtifact * art)
{
if(art->artType->getId() == ArtifactID::SPELL_SCROLL)
if(art->getId() == ArtifactID::SPELL_SCROLL)
return 1500;
auto statsValue =
@ -267,8 +262,10 @@ static uint64_t evaluateArtifactArmyValue(const CArtifactInstance * art)
auto classValue = 0;
switch(art->artType->aClass)
switch(art->aClass)
{
case CArtifact::EartClass::ART_TREASURE:
//FALL_THROUGH
case CArtifact::EartClass::ART_MINOR:
classValue = 1000;
break;
@ -302,22 +299,15 @@ uint64_t RewardEvaluator::getArmyReward(
{
case Obj::HILL_FORT:
return ai->armyManager->calculateCreaturesUpgrade(army, target, ai->cb->getResourceAmount()).upgradeValue;
case Obj::CREATURE_BANK:
return getCreatureBankArmyReward(target, hero);
case Obj::CREATURE_GENERATOR1:
case Obj::CREATURE_GENERATOR2:
case Obj::CREATURE_GENERATOR3:
case Obj::CREATURE_GENERATOR4:
return getDwellingArmyValue(ai->cb.get(), target, checkGold);
case Obj::CRYPT:
case Obj::SHIPWRECK:
case Obj::SHIPWRECK_SURVIVOR:
case Obj::WARRIORS_TOMB:
return 1000;
case Obj::SPELL_SCROLL:
//FALL_THROUGH
case Obj::ARTIFACT:
return evaluateArtifactArmyValue(dynamic_cast<const CGArtifact *>(target)->storedArtifact);
case Obj::DRAGON_UTOPIA:
return 10000;
return evaluateArtifactArmyValue(dynamic_cast<const CGArtifact *>(target)->storedArtifact->getType());
case Obj::HERO:
return relations == PlayerRelations::ENEMIES
? enemyArmyEliminationRewardRatio * dynamic_cast<const CGHeroInstance *>(target)->getArmyStrength()
@ -328,8 +318,46 @@ uint64_t RewardEvaluator::getArmyReward(
case Obj::MAGIC_SPRING:
return getManaRecoveryArmyReward(hero);
default:
return 0;
break;
}
auto rewardable = dynamic_cast<const Rewardable::Interface *>(target);
if(rewardable)
{
auto totalValue = 0;
for(int index : rewardable->getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT))
{
auto & info = rewardable->configuration.info[index];
auto rewardValue = 0;
if(!info.reward.artifacts.empty())
{
for(auto artID : info.reward.artifacts)
{
const auto * art = dynamic_cast<const CArtifact *>(VLC->artifacts()->getById(artID));
rewardValue += evaluateArtifactArmyValue(art);
}
}
if(!info.reward.creatures.empty())
{
for(const auto & stackInfo : info.reward.creatures)
{
rewardValue += stackInfo.getType()->getAIValue() * stackInfo.getCount();
}
}
totalValue += rewardValue > 0 ? rewardValue / (info.reward.artifacts.size() + info.reward.creatures.size()) : 0;
}
return totalValue;
}
return 0;
}
uint64_t RewardEvaluator::getArmyGrowth(
@ -468,12 +496,29 @@ uint64_t RewardEvaluator::townArmyGrowth(const CGTownInstance * town) const
return result;
}
uint64_t RewardEvaluator::getManaRecoveryArmyReward(const CGHeroInstance * hero) const
float RewardEvaluator::getManaRecoveryArmyReward(const CGHeroInstance * hero) const
{
return ai->heroManager->getMagicStrength(hero) * 10000 * (1.0f - std::sqrt(static_cast<float>(hero->mana) / hero->manaLimit()));
}
float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) const
float RewardEvaluator::getResourceRequirementStrength(const TResources & res) const
{
float sum = 0.0f;
for(TResources::nziterator it(res); it.valid(); it++)
{
//Evaluate resources used for construction. Gold is evaluated separately.
if(it->resType != EGameResID::GOLD)
{
sum += 0.1f * it->resVal * getResourceRequirementStrength(it->resType)
+ 0.05f * it->resVal * getTotalResourceRequirementStrength(it->resType);
}
}
return sum;
}
float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target, const CGHeroInstance * hero) const
{
if(!target)
return 0;
@ -491,24 +536,10 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons
case Obj::RESOURCE:
{
auto resource = dynamic_cast<const CGResource *>(target);
return resource->resourceID() == EGameResID::GOLD
? 0
: 0.2f * getTotalResourceRequirementStrength(resource->resourceID()) + 0.4f * getResourceRequirementStrength(resource->resourceID());
}
case Obj::CREATURE_BANK:
{
auto resourceReward = getCreatureBankResources(target, nullptr);
float sum = 0.0f;
for (TResources::nziterator it (resourceReward); it.valid(); it++)
{
//Evaluate resources used for construction. Gold is evaluated separately.
if (it->resType != EGameResID::GOLD)
{
sum += 0.1f * it->resVal * getResourceRequirementStrength(it->resType);
}
}
return sum;
TResources res;
res[resource->resourceID()] = resource->amount;
return getResourceRequirementStrength(res);
}
case Obj::TOWN:
@ -546,6 +577,70 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons
case Obj::KEYMASTER:
return 0.6f;
default:
break;
}
auto rewardable = dynamic_cast<const Rewardable::Interface *>(target);
if(rewardable && hero)
{
auto resourceReward = 0.0f;
for(int index : rewardable->getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT))
{
resourceReward += getResourceRequirementStrength(rewardable->configuration.info[index].reward.resources);
}
return resourceReward;
}
return 0;
}
float RewardEvaluator::getConquestValue(const CGObjectInstance* target) const
{
if (!target)
return 0;
if (target->getOwner() == ai->playerID)
return 0;
switch (target->ID)
{
case Obj::TOWN:
{
if (ai->buildAnalyzer->getDevelopmentInfo().empty())
return 10.0f;
auto town = dynamic_cast<const CGTownInstance*>(target);
if (town->getOwner() == ai->playerID)
{
auto armyIncome = townArmyGrowth(town);
auto dailyIncome = town->dailyIncome()[EGameResID::GOLD];
return std::min(1.0f, std::sqrt(armyIncome / 40000.0f)) + std::min(0.3f, dailyIncome / 10000.0f);
}
auto fortLevel = town->fortLevel();
auto booster = 1.0f;
if (town->hasCapitol())
return booster * 1.5;
if (fortLevel < CGTownInstance::CITADEL)
return booster * (town->hasFort() ? 1.0 : 0.8);
else
return booster * (fortLevel == CGTownInstance::CASTLE ? 1.4 : 1.2);
}
case Obj::HERO:
return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES
? getEnemyHeroStrategicalValue(dynamic_cast<const CGHeroInstance*>(target))
: 0;
case Obj::KEYMASTER:
return 0.6f;
default:
return 0;
}
@ -593,11 +688,11 @@ float RewardEvaluator::getSkillReward(const CGObjectInstance * target, const CGH
case Obj::ARENA:
return 2;
case Obj::SHRINE_OF_MAGIC_INCANTATION:
return 0.2f;
return 0.25f;
case Obj::SHRINE_OF_MAGIC_GESTURE:
return 0.3f;
return 1.0f;
case Obj::SHRINE_OF_MAGIC_THOUGHT:
return 0.5f;
return 2.0f;
case Obj::LIBRARY_OF_ENLIGHTENMENT:
return 8;
case Obj::WITCH_HUT:
@ -605,15 +700,55 @@ float RewardEvaluator::getSkillReward(const CGObjectInstance * target, const CGH
case Obj::PANDORAS_BOX:
//Can contains experience, spells, or skills (only on custom maps)
return 2.5f;
case Obj::PYRAMID:
return 3.0f;
case Obj::HERO:
return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES
? enemyHeroEliminationSkillRewardRatio * dynamic_cast<const CGHeroInstance *>(target)->level
: 0;
default:
return 0;
break;
}
auto rewardable = dynamic_cast<const Rewardable::Interface *>(target);
if(rewardable)
{
auto totalValue = 0.0f;
for(int index : rewardable->getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT))
{
auto & info = rewardable->configuration.info[index];
auto rewardValue = 0.0f;
if(!info.reward.spells.empty())
{
for(auto spellID : info.reward.spells)
{
const spells::Spell * spell = VLC->spells()->getById(spellID);
if(hero->canLearnSpell(spell) && !hero->spellbookContainsSpell(spellID))
{
rewardValue += std::sqrt(spell->getLevel()) / 4.0f;
}
}
totalValue += rewardValue / info.reward.spells.size();
}
if(!info.reward.primary.empty())
{
for(auto value : info.reward.primary)
{
totalValue += value;
}
}
}
return totalValue;
}
return 0;
}
const HitMapInfo & RewardEvaluator::getEnemyHeroDanger(const int3 & tile, uint8_t turn) const
@ -635,7 +770,7 @@ int32_t getArmyCost(const CArmedInstance * army)
for(auto stack : army->Slots())
{
value += stack.second->getCreatureID().toCreature()->getRecruitCost(EGameResID::GOLD) * stack.second->count;
value += stack.second->getCreatureID().toCreature()->getFullRecruitCost().marketValue() * stack.second->count;
}
return value;
@ -671,22 +806,6 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG
auto * mine = dynamic_cast<const CGMine*>(target);
return dailyIncomeMultiplier * (mine->producedResource == GameResID::GOLD ? 1000 : 75);
}
case Obj::MYSTICAL_GARDEN:
case Obj::WINDMILL:
return 100;
case Obj::CAMPFIRE:
return 800;
case Obj::WAGON:
return 100;
case Obj::CREATURE_BANK:
return getResourcesGoldReward(getCreatureBankResources(target, hero));
case Obj::CRYPT:
case Obj::DERELICT_SHIP:
return 3000;
case Obj::DRAGON_UTOPIA:
return 10000;
case Obj::SEA_CHEST:
return 1500;
case Obj::PANDORAS_BOX:
return 2500;
case Obj::PRISON:
@ -697,8 +816,26 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG
? heroEliminationBonus + enemyArmyEliminationGoldRewardRatio * getArmyCost(dynamic_cast<const CGHeroInstance *>(target))
: 0;
default:
return 0;
break;
}
auto rewardable = dynamic_cast<const Rewardable::Interface *>(target);
if(rewardable)
{
auto goldReward = 0;
for(int index : rewardable->getAvailableRewards(hero, Rewardable::EEventType::EVENT_FIRST_VISIT))
{
auto & info = rewardable->configuration.info[index];
goldReward += getResourcesGoldReward(info.reward.resources);
}
return goldReward;
}
return 0;
}
class HeroExchangeEvaluator : public IEvaluationContextBuilder
@ -714,7 +851,9 @@ public:
uint64_t armyStrength = heroExchange.getReinforcementArmyStrength(evaluationContext.evaluator.ai);
evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength());
evaluationContext.conquestValue += 2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength();
evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero);
evaluationContext.isExchange = true;
}
};
@ -732,6 +871,7 @@ public:
evaluationContext.armyReward += upgradeValue;
evaluationContext.addNonCriticalStrategicalValue(upgradeValue / (float)armyUpgrade.hero->getArmyStrength());
evaluationContext.isArmyUpgrade = true;
}
};
@ -746,22 +886,46 @@ public:
int tilesDiscovered = task->value;
evaluationContext.addNonCriticalStrategicalValue(0.03f * tilesDiscovered);
for (auto obj : evaluationContext.evaluator.ai->cb->getVisitableObjs(task->tile))
{
switch (obj->ID.num)
{
case Obj::MONOLITH_ONE_WAY_ENTRANCE:
case Obj::MONOLITH_TWO_WAY:
case Obj::SUBTERRANEAN_GATE:
evaluationContext.explorePriority = 1;
break;
case Obj::REDWOOD_OBSERVATORY:
case Obj::PILLAR_OF_FIRE:
evaluationContext.explorePriority = 2;
break;
}
}
if(evaluationContext.evaluator.ai->cb->getTile(task->tile)->roadType != RoadId::NO_ROAD)
evaluationContext.explorePriority = 1;
if (evaluationContext.explorePriority == 0)
evaluationContext.explorePriority = 3;
}
};
class StayAtTownManaRecoveryEvaluator : public IEvaluationContextBuilder
{
public:
void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override
void buildEvaluationContext(EvaluationContext& evaluationContext, Goals::TSubgoal task) const override
{
if(task->goalType != Goals::STAY_AT_TOWN)
if (task->goalType != Goals::STAY_AT_TOWN)
return;
Goals::StayAtTown & stayAtTown = dynamic_cast<Goals::StayAtTown &>(*task);
Goals::StayAtTown& stayAtTown = dynamic_cast<Goals::StayAtTown&>(*task);
evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero());
evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted();
evaluationContext.movementCost += stayAtTown.getMovementWasted();
if (evaluationContext.armyReward == 0)
evaluationContext.isDefend = true;
else
{
evaluationContext.movementCost += stayAtTown.getMovementWasted();
evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted();
}
}
};
@ -772,15 +936,8 @@ void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uin
if(enemyDanger.danger)
{
auto dangerRatio = enemyDanger.danger / (double)ourStrength;
auto enemyHero = evaluationContext.evaluator.ai->cb->getObj(enemyDanger.hero.hid, false);
bool isAI = enemyHero && isAnotherAi(enemyHero, *evaluationContext.evaluator.ai->cb);
if(isAI)
{
dangerRatio *= 1.5; // lets make AI bit more afraid of other AI.
}
vstd::amax(evaluationContext.enemyHeroDangerRatio, dangerRatio);
vstd::amax(evaluationContext.threat, enemyDanger.threat);
}
}
@ -824,6 +981,10 @@ public:
else
evaluationContext.addNonCriticalStrategicalValue(1.7f * multiplier * strategicalValue);
evaluationContext.defenseValue = town->fortLevel();
evaluationContext.isDefend = true;
evaluationContext.threatTurns = treat.turn;
vstd::amax(evaluationContext.danger, defendTown.getTreat().danger);
addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength());
}
@ -845,6 +1006,9 @@ public:
Goals::ExecuteHeroChain & chain = dynamic_cast<Goals::ExecuteHeroChain &>(*task);
const AIPath & path = chain.getPath();
if (vstd::isAlmostZero(path.movementCost()))
return;
vstd::amax(evaluationContext.danger, path.getTotalDanger());
evaluationContext.movementCost += path.movementCost();
evaluationContext.closestWayRatio = chain.closestWayRatio;
@ -854,14 +1018,24 @@ public:
for(auto & node : path.nodes)
{
vstd::amax(costsPerHero[node.targetHero], node.cost);
if (node.layer == EPathfindingLayer::SAIL)
evaluationContext.involvesSailing = true;
}
float highestCostForSingleHero = 0;
for(auto pair : costsPerHero)
{
auto role = evaluationContext.evaluator.ai->heroManager->getHeroRole(pair.first);
evaluationContext.movementCostByRole[role] += pair.second;
if (pair.second > highestCostForSingleHero)
highestCostForSingleHero = pair.second;
}
if (highestCostForSingleHero > 1 && costsPerHero.size() > 1)
{
//Chains that involve more than 1 hero doing something for more than a turn are too expensive in my book. They often involved heroes doing nothing just standing there waiting to fulfill their part of the chain.
return;
}
evaluationContext.movementCost *= costsPerHero.size(); //further deincentivise chaining as it often involves bringing back the army afterwards
auto hero = task->hero;
bool checkGold = evaluationContext.danger == 0;
@ -880,10 +1054,18 @@ public:
evaluationContext.armyGrowth += evaluationContext.evaluator.getArmyGrowth(target, hero, army);
evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, heroRole);
evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target));
evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target);
if (target->ID == Obj::HERO)
evaluationContext.isHero = true;
if (target->getOwner().isValidPlayer() && ai->cb->getPlayerRelations(ai->playerID, target->getOwner()) == PlayerRelations::ENEMIES)
evaluationContext.isEnemy = true;
evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army);
if(evaluationContext.danger > 0)
evaluationContext.skillReward += (float)evaluationContext.danger / (float)hero->getArmyStrength();
}
evaluationContext.armyInvolvement += army->getArmyCost();
vstd::amax(evaluationContext.armyLossPersentage, path.getTotalArmyLoss() / (double)path.getHeroStrength());
vstd::amax(evaluationContext.armyLossPersentage, (float)path.getTotalArmyLoss() / (float)army->getArmyStrength());
addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength());
vstd::amax(evaluationContext.turn, path.turn());
}
@ -924,6 +1106,7 @@ public:
evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold) / boost;
evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, role) / boost;
evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target) / boost);
evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target);
evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army) / boost;
evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost;
evaluationContext.movementCost += objInfo.second.movementCost / boost;
@ -949,6 +1132,14 @@ public:
Goals::ExchangeSwapTownHeroes & swapCommand = dynamic_cast<Goals::ExchangeSwapTownHeroes &>(*task);
const CGHeroInstance * garrisonHero = swapCommand.getGarrisonHero();
logAi->trace("buildEvaluationContext ExchangeSwapTownHeroesContextBuilder %s affected objects: %d", swapCommand.toString(), swapCommand.getAffectedObjects().size());
for (auto obj : swapCommand.getAffectedObjects())
{
logAi->trace("affected object: %s", evaluationContext.evaluator.ai->cb->getObj(obj)->getObjectName());
}
if (garrisonHero)
logAi->debug("with %s and %d", garrisonHero->getNameTranslated(), int(swapCommand.getLockingReason()));
if(garrisonHero && swapCommand.getLockingReason() == HeroLockedReason::DEFENCE)
{
auto defenderRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(garrisonHero);
@ -957,6 +1148,9 @@ public:
evaluationContext.movementCost += mpLeft;
evaluationContext.movementCostByRole[defenderRole] += mpLeft;
evaluationContext.heroRole = defenderRole;
evaluationContext.isDefend = true;
evaluationContext.armyInvolvement = garrisonHero->getArmyStrength();
logAi->debug("evaluationContext.isDefend: %d", evaluationContext.isDefend);
}
}
};
@ -1000,8 +1194,14 @@ public:
evaluationContext.goldReward += 7 * bi.dailyIncome[EGameResID::GOLD] / 2; // 7 day income but half we already have
evaluationContext.heroRole = HeroRole::MAIN;
evaluationContext.movementCostByRole[evaluationContext.heroRole] += bi.prerequisitesCount;
evaluationContext.goldCost += bi.buildCostWithPrerequisits[EGameResID::GOLD];
int32_t cost = bi.buildCost[EGameResID::GOLD];
evaluationContext.goldCost += cost;
evaluationContext.closestWayRatio = 1;
evaluationContext.buildingCost += bi.buildCostWithPrerequisites;
if (bi.id == BuildingID::MARKETPLACE || bi.dailyIncome[EGameResID::WOOD] > 0)
evaluationContext.isTradeBuilding = true;
logAi->trace("Building costs for %s : %s MarketValue: %d",bi.toString(), evaluationContext.buildingCost.toString(), evaluationContext.buildingCost.marketValue());
if(bi.creatureID != CreatureID::NONE)
{
@ -1028,7 +1228,18 @@ public:
else if(bi.id >= BuildingID::MAGES_GUILD_1 && bi.id <= BuildingID::MAGES_GUILD_5)
{
evaluationContext.skillReward += 2 * (bi.id - BuildingID::MAGES_GUILD_1);
for (auto hero : evaluationContext.evaluator.ai->cb->getHeroesInfo())
{
evaluationContext.armyInvolvement += hero->getArmyCost();
}
}
int sameTownBonus = 0;
for (auto town : evaluationContext.evaluator.ai->cb->getTownsInfo())
{
if (buildThis.town->getFaction() == town->getFaction())
sameTownBonus += town->getTownLevel();
}
evaluationContext.armyReward *= sameTownBonus;
if(evaluationContext.goldReward)
{
@ -1048,7 +1259,7 @@ public:
uint64_t RewardEvaluator::getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const
{
if(ai->buildAnalyzer->hasAnyBuilding(town->getFaction(), bi.id))
if(ai->buildAnalyzer->hasAnyBuilding(town->getFactionID(), bi.id))
return 0;
auto creaturesToUpgrade = ai->armyManager->getTotalCreaturesAvailable(bi.baseCreatureID);
@ -1090,6 +1301,7 @@ EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal
for(auto subgoal : parts)
{
context.goldCost += subgoal->goldCost;
context.buildingCost += subgoal->buildingCost;
for(auto builder : evaluationContextBuilders)
{
@ -1100,7 +1312,7 @@ EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal
return context;
}
float PriorityEvaluator::evaluate(Goals::TSubgoal task)
float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
{
auto evaluationContext = buildEvaluationContext(task);
@ -1113,47 +1325,284 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
double result = 0;
try
if (ai->settings->isUseFuzzy())
{
armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage);
heroRoleVariable->setValue(evaluationContext.heroRole);
mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]);
scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]);
goldRewardVariable->setValue(goldRewardPerTurn);
armyRewardVariable->setValue(evaluationContext.armyReward);
armyGrowthVariable->setValue(evaluationContext.armyGrowth);
skillRewardVariable->setValue(evaluationContext.skillReward);
dangerVariable->setValue(evaluationContext.danger);
rewardTypeVariable->setValue(rewardType);
closestHeroRatioVariable->setValue(evaluationContext.closestWayRatio);
strategicalValueVariable->setValue(evaluationContext.strategicalValue);
goldPressureVariable->setValue(ai->buildAnalyzer->getGoldPressure());
goldCostVariable->setValue(evaluationContext.goldCost / ((float)ai->getFreeResources()[EGameResID::GOLD] + (float)ai->buildAnalyzer->getDailyIncome()[EGameResID::GOLD] + 1.0f));
turnVariable->setValue(evaluationContext.turn);
fearVariable->setValue(evaluationContext.enemyHeroDangerRatio);
float fuzzyResult = 0;
try
{
armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage);
heroRoleVariable->setValue(evaluationContext.heroRole);
mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]);
scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]);
goldRewardVariable->setValue(goldRewardPerTurn);
armyRewardVariable->setValue(evaluationContext.armyReward);
armyGrowthVariable->setValue(evaluationContext.armyGrowth);
skillRewardVariable->setValue(evaluationContext.skillReward);
dangerVariable->setValue(evaluationContext.danger);
rewardTypeVariable->setValue(rewardType);
closestHeroRatioVariable->setValue(evaluationContext.closestWayRatio);
strategicalValueVariable->setValue(evaluationContext.strategicalValue);
goldPressureVariable->setValue(ai->buildAnalyzer->getGoldPressure());
goldCostVariable->setValue(evaluationContext.goldCost / ((float)ai->getFreeResources()[EGameResID::GOLD] + (float)ai->buildAnalyzer->getDailyIncome()[EGameResID::GOLD] + 1.0f));
turnVariable->setValue(evaluationContext.turn);
fearVariable->setValue(evaluationContext.enemyHeroDangerRatio);
engine->process();
engine->process();
result = value->getValue();
fuzzyResult = value->getValue();
}
catch (fl::Exception& fe)
{
logAi->error("evaluate VisitTile: %s", fe.getWhat());
}
result = fuzzyResult;
}
catch(fl::Exception & fe)
else
{
logAi->error("evaluate VisitTile: %s", fe.getWhat());
float score = 0;
const bool amIInDanger = ai->cb->getTownsInfo().empty() || (evaluationContext.isDefend && evaluationContext.threatTurns == 0);
const float maxWillingToLose = amIInDanger ? 1 : ai->settings->getMaxArmyLossTarget();
bool arriveNextWeek = false;
if (ai->cb->getDate(Date::DAY_OF_WEEK) + evaluationContext.turn > 7 && priorityTier < PriorityTier::FAR_KILL)
arriveNextWeek = true;
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("BEFORE: priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, army-involvement: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, explorePriority: %d isDefend: %d isEnemy: %d arriveNextWeek: %d",
priorityTier,
task->toString(),
evaluationContext.armyLossPersentage,
(int)evaluationContext.turn,
evaluationContext.movementCostByRole[HeroRole::MAIN],
evaluationContext.movementCostByRole[HeroRole::SCOUT],
evaluationContext.armyInvolvement,
goldRewardPerTurn,
evaluationContext.goldCost,
evaluationContext.armyReward,
evaluationContext.armyGrowth,
evaluationContext.skillReward,
evaluationContext.danger,
evaluationContext.threatTurns,
evaluationContext.threat,
evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout",
evaluationContext.strategicalValue,
evaluationContext.conquestValue,
evaluationContext.closestWayRatio,
evaluationContext.enemyHeroDangerRatio,
evaluationContext.explorePriority,
evaluationContext.isDefend,
evaluationContext.isEnemy,
arriveNextWeek);
#endif
switch (priorityTier)
{
case PriorityTier::INSTAKILL: //Take towns / kill heroes in immediate reach
{
if (evaluationContext.turn > 0)
return 0;
if (evaluationContext.movementCost >= 1)
return 0;
if(evaluationContext.conquestValue > 0)
score = evaluationContext.armyInvolvement;
if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty()))
return 0;
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0;
if (evaluationContext.movementCost > 0)
score /= evaluationContext.movementCost;
break;
}
case PriorityTier::INSTADEFEND: //Defend immediately threatened towns
{
if (evaluationContext.isDefend && evaluationContext.threatTurns == 0 && evaluationContext.turn == 0)
score = evaluationContext.armyInvolvement;
if (evaluationContext.isEnemy && maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0;
break;
}
case PriorityTier::KILL: //Take towns / kill heroes that are further away
//FALL_THROUGH
case PriorityTier::FAR_KILL:
{
if (evaluationContext.turn > 0 && evaluationContext.isHero)
return 0;
if (arriveNextWeek && evaluationContext.isEnemy)
return 0;
if (evaluationContext.conquestValue > 0)
score = evaluationContext.armyInvolvement;
if (vstd::isAlmostZero(score) || (evaluationContext.enemyHeroDangerRatio > 1 && (evaluationContext.turn > 0 || evaluationContext.isExchange) && !ai->cb->getTownsInfo().empty()))
return 0;
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0;
score *= evaluationContext.closestWayRatio;
if (evaluationContext.movementCost > 0)
score /= evaluationContext.movementCost;
break;
}
case PriorityTier::UPGRADE:
{
if (!evaluationContext.isArmyUpgrade)
return 0;
if (evaluationContext.enemyHeroDangerRatio > 1)
return 0;
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0;
if (vstd::isAlmostZero(evaluationContext.armyLossPersentage) && evaluationContext.closestWayRatio < 1.0)
return 0;
score = 1000;
if (evaluationContext.movementCost > 0)
score /= evaluationContext.movementCost;
break;
}
case PriorityTier::HIGH_PRIO_EXPLORE:
{
if (evaluationContext.enemyHeroDangerRatio > 1)
return 0;
if (evaluationContext.explorePriority != 1)
return 0;
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0;
if (vstd::isAlmostZero(evaluationContext.armyLossPersentage) && evaluationContext.closestWayRatio < 1.0)
return 0;
score = 1000;
if (evaluationContext.movementCost > 0)
score /= evaluationContext.movementCost;
break;
}
case PriorityTier::HUNTER_GATHER: //Collect guarded stuff
//FALL_THROUGH
case PriorityTier::FAR_HUNTER_GATHER:
{
if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend)
return 0;
if (evaluationContext.buildingCost.marketValue() > 0)
return 0;
if (evaluationContext.isDefend && (evaluationContext.enemyHeroDangerRatio < 1 || evaluationContext.threatTurns > 0 || evaluationContext.turn > 0))
return 0;
if (evaluationContext.explorePriority == 3)
return 0;
if (evaluationContext.isArmyUpgrade)
return 0;
if ((evaluationContext.enemyHeroDangerRatio > 0 && arriveNextWeek) || evaluationContext.enemyHeroDangerRatio > 1)
return 0;
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0;
if (vstd::isAlmostZero(evaluationContext.armyLossPersentage) && evaluationContext.closestWayRatio < 1.0)
return 0;
score += evaluationContext.strategicalValue * 1000;
score += evaluationContext.goldReward;
score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05;
score += evaluationContext.armyReward;
score += evaluationContext.armyGrowth;
score -= evaluationContext.goldCost;
score -= evaluationContext.armyInvolvement * evaluationContext.armyLossPersentage;
if (score > 0)
{
score = 1000;
if (evaluationContext.movementCost > 0)
score /= evaluationContext.movementCost;
}
break;
}
case PriorityTier::LOW_PRIO_EXPLORE:
{
if (evaluationContext.enemyHeroDangerRatio > 1)
return 0;
if (evaluationContext.explorePriority != 3)
return 0;
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0;
if (evaluationContext.closestWayRatio < 1.0)
return 0;
score = 1000;
if (evaluationContext.movementCost > 0)
score /= evaluationContext.movementCost;
break;
}
case PriorityTier::DEFEND: //Defend whatever if nothing else is to do
{
if (evaluationContext.enemyHeroDangerRatio > 1 && evaluationContext.isExchange)
return 0;
if (evaluationContext.isDefend || evaluationContext.isArmyUpgrade)
score = evaluationContext.armyInvolvement;
score /= (evaluationContext.turn + 1);
break;
}
case PriorityTier::BUILDINGS: //For buildings and buying army
{
if (maxWillingToLose - evaluationContext.armyLossPersentage < 0)
return 0;
//If we already have locked resources, we don't look at other buildings
if (ai->getLockedResources().marketValue() > 0)
return 0;
score += evaluationContext.conquestValue * 1000;
score += evaluationContext.strategicalValue * 1000;
score += evaluationContext.goldReward;
score += evaluationContext.skillReward * evaluationContext.armyInvolvement * (1 - evaluationContext.armyLossPersentage) * 0.05;
score += evaluationContext.armyReward;
score += evaluationContext.armyGrowth;
if (evaluationContext.buildingCost.marketValue() > 0)
{
if (!evaluationContext.isTradeBuilding && ai->getFreeResources()[EGameResID::WOOD] - evaluationContext.buildingCost[EGameResID::WOOD] < 5 && ai->buildAnalyzer->getDailyIncome()[EGameResID::WOOD] < 1)
{
logAi->trace("Should make sure to build market-place instead of %s", task->toString());
for (auto town : ai->cb->getTownsInfo())
{
if (!town->hasBuiltSomeTradeBuilding())
return 0;
}
}
score += 1000;
auto resourcesAvailable = evaluationContext.evaluator.ai->getFreeResources();
auto income = ai->buildAnalyzer->getDailyIncome();
if(ai->buildAnalyzer->isGoldPressureHigh())
score /= evaluationContext.buildingCost.marketValue();
if (!resourcesAvailable.canAfford(evaluationContext.buildingCost))
{
TResources needed = evaluationContext.buildingCost - resourcesAvailable;
needed.positive();
int turnsTo = needed.maxPurchasableCount(income);
if (turnsTo == INT_MAX)
return 0;
else
score /= turnsTo;
}
}
else
{
if (evaluationContext.enemyHeroDangerRatio > 1 && !evaluationContext.isDefend && vstd::isAlmostZero(evaluationContext.conquestValue))
return 0;
}
break;
}
}
result = score;
//TODO: Figure out the root cause for why evaluationContext.closestWayRatio has become -nan(ind).
if (std::isnan(result))
return 0;
}
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, gold: %f, cost: %d, army gain: %f, danger: %d, role: %s, strategical value: %f, cwr: %f, fear: %f, result %f",
logAi->trace("priorityTier %d, Evaluated %s, loss: %f, turn: %d, turns main: %f, scout: %f, army-involvement: %f, gold: %f, cost: %d, army gain: %f, army growth: %f skill: %f danger: %d, threatTurns: %d, threat: %d, role: %s, strategical value: %f, conquest value: %f cwr: %f, fear: %f, result %f",
priorityTier,
task->toString(),
evaluationContext.armyLossPersentage,
(int)evaluationContext.turn,
evaluationContext.movementCostByRole[HeroRole::MAIN],
evaluationContext.movementCostByRole[HeroRole::SCOUT],
evaluationContext.armyInvolvement,
goldRewardPerTurn,
evaluationContext.goldCost,
evaluationContext.armyReward,
evaluationContext.armyGrowth,
evaluationContext.skillReward,
evaluationContext.danger,
evaluationContext.threatTurns,
evaluationContext.threat,
evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout",
evaluationContext.strategicalValue,
evaluationContext.conquestValue,
evaluationContext.closestWayRatio,
evaluationContext.enemyHeroDangerRatio,
result);

View File

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

View File

@ -11,6 +11,8 @@
#include <limits>
#include "Settings.h"
#include "../../../lib/constants/StringConstants.h"
#include "../../../lib/mapObjectConstructors/AObjectTypeHandler.h"
#include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h"
#include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
@ -22,56 +24,42 @@
namespace NKAI
{
Settings::Settings()
Settings::Settings(int difficultyLevel)
: maxRoamingHeroes(8),
mainHeroTurnDistanceLimit(10),
scoutHeroTurnDistanceLimit(5),
maxGoldPressure(0.3f),
maxGoldPressure(0.3f),
retreatThresholdRelative(0.3),
retreatThresholdAbsolute(10000),
safeAttackRatio(1.1),
maxpass(10),
pathfinderBucketsCount(1),
pathfinderBucketSize(32),
allowObjectGraph(true),
useTroopsFromGarrisons(false),
openMap(true)
updateHitmapOnTileReveal(false),
openMap(true),
useFuzzy(false)
{
JsonNode node = JsonUtils::assembleFromFiles("config/ai/nkai/nkai-settings");
const std::string & difficultyName = GameConstants::DIFFICULTY_NAMES[difficultyLevel];
const JsonNode & rootNode = JsonUtils::assembleFromFiles("config/ai/nkai/nkai-settings");
const JsonNode & node = rootNode[difficultyName];
if(node.Struct()["maxRoamingHeroes"].isNumber())
{
maxRoamingHeroes = node.Struct()["maxRoamingHeroes"].Integer();
}
if(node.Struct()["mainHeroTurnDistanceLimit"].isNumber())
{
mainHeroTurnDistanceLimit = node.Struct()["mainHeroTurnDistanceLimit"].Integer();
}
if(node.Struct()["scoutHeroTurnDistanceLimit"].isNumber())
{
scoutHeroTurnDistanceLimit = node.Struct()["scoutHeroTurnDistanceLimit"].Integer();
}
if(node.Struct()["maxpass"].isNumber())
{
maxpass = node.Struct()["maxpass"].Integer();
}
if(node.Struct()["maxGoldPressure"].isNumber())
{
maxGoldPressure = node.Struct()["maxGoldPressure"].Float();
}
if(!node.Struct()["allowObjectGraph"].isNull())
{
allowObjectGraph = node.Struct()["allowObjectGraph"].Bool();
}
if(!node.Struct()["openMap"].isNull())
{
openMap = node.Struct()["openMap"].Bool();
}
if(!node.Struct()["useTroopsFromGarrisons"].isNull())
{
useTroopsFromGarrisons = node.Struct()["useTroopsFromGarrisons"].Bool();
}
maxRoamingHeroes = node["maxRoamingHeroes"].Integer();
mainHeroTurnDistanceLimit = node["mainHeroTurnDistanceLimit"].Integer();
scoutHeroTurnDistanceLimit = node["scoutHeroTurnDistanceLimit"].Integer();
maxpass = node["maxpass"].Integer();
pathfinderBucketsCount = node["pathfinderBucketsCount"].Integer();
pathfinderBucketSize = node["pathfinderBucketSize"].Integer();
maxGoldPressure = node["maxGoldPressure"].Float();
retreatThresholdRelative = node["retreatThresholdRelative"].Float();
retreatThresholdAbsolute = node["retreatThresholdAbsolute"].Float();
maxArmyLossTarget = node["maxArmyLossTarget"].Float();
safeAttackRatio = node["safeAttackRatio"].Float();
allowObjectGraph = node["allowObjectGraph"].Bool();
updateHitmapOnTileReveal = node["updateHitmapOnTileReveal"].Bool();
openMap = node["openMap"].Bool();
useFuzzy = node["useFuzzy"].Bool();
useTroopsFromGarrisons = node["useTroopsFromGarrisons"].Bool();
}
}

View File

@ -25,21 +25,37 @@ namespace NKAI
int mainHeroTurnDistanceLimit;
int scoutHeroTurnDistanceLimit;
int maxpass;
int pathfinderBucketsCount;
int pathfinderBucketSize;
float maxGoldPressure;
float retreatThresholdRelative;
float retreatThresholdAbsolute;
float safeAttackRatio;
float maxArmyLossTarget;
bool allowObjectGraph;
bool useTroopsFromGarrisons;
bool updateHitmapOnTileReveal;
bool openMap;
bool useFuzzy;
public:
Settings();
explicit Settings(int difficultyLevel);
int getMaxPass() const { return maxpass; }
float getMaxGoldPressure() const { return maxGoldPressure; }
float getRetreatThresholdRelative() const { return retreatThresholdRelative; }
float getRetreatThresholdAbsolute() const { return retreatThresholdAbsolute; }
float getSafeAttackRatio() const { return safeAttackRatio; }
float getMaxArmyLossTarget() const { return maxArmyLossTarget; }
int getMaxRoamingHeroes() const { return maxRoamingHeroes; }
int getMainHeroTurnDistanceLimit() const { return mainHeroTurnDistanceLimit; }
int getScoutHeroTurnDistanceLimit() const { return scoutHeroTurnDistanceLimit; }
int getPathfinderBucketsCount() const { return pathfinderBucketsCount; }
int getPathfinderBucketSize() const { return pathfinderBucketSize; }
bool isObjectGraphAllowed() const { return allowObjectGraph; }
bool isGarrisonTroopsUsageAllowed() const { return useTroopsFromGarrisons; }
bool isUpdateHitmapOnTileReveal() const { return updateHitmapOnTileReveal; }
bool isOpenMap() const { return openMap; }
bool isUseFuzzy() const { return useFuzzy; }
};
}

View File

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

View File

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

View File

@ -12,7 +12,7 @@
#include "../AIGateway.h"
#include "../AIUtility.h"
#include "../../../lib/constants/StringConstants.h"
#include "../../../lib/entities/building/CBuilding.h"
namespace NKAI
{
@ -23,7 +23,7 @@ BuildThis::BuildThis(BuildingID Bid, const CGTownInstance * tid)
: ElementarGoal(Goals::BUILD_STRUCTURE)
{
buildingInfo = BuildingInfo(
tid->town->buildings.at(Bid),
tid->getTown()->buildings.at(Bid),
nullptr,
CreatureID::NONE,
tid,
@ -52,7 +52,7 @@ void BuildThis::accept(AIGateway * ai)
if(cb->canBuildStructure(town, b) == EBuildingState::ALLOWED)
{
logAi->debug("Player %d will build %s in town of %s at %s",
ai->playerID, town->town->buildings.at(b)->getNameTranslated(), town->getNameTranslated(), town->pos.toString());
ai->playerID, town->getTown()->buildings.at(b)->getNameTranslated(), town->getNameTranslated(), town->anchorPos().toString());
cb->buildBuilding(town, b);
return;

View File

@ -34,13 +34,13 @@ void BuyArmy::accept(AIGateway * ai)
ui64 valueBought = 0;
//buy the stacks with largest AI value
auto upgradeSuccessfull = ai->makePossibleUpgrades(town);
auto upgradeSuccessful = ai->makePossibleUpgrades(town);
auto armyToBuy = ai->nullkiller->armyManager->getArmyAvailableToBuy(town->getUpperArmy(), town);
if(armyToBuy.empty())
{
if(upgradeSuccessfull)
if(upgradeSuccessful)
return;
throw cannotFulfillGoalException("No creatures to buy.");
@ -58,7 +58,36 @@ void BuyArmy::accept(AIGateway * ai)
if(ci.count)
{
cb->recruitCreatures(town, town->getUpperArmy(), ci.creID, ci.count, ci.level);
if (town->getUpperArmy()->stacksCount() == GameConstants::ARMY_SIZE)
{
SlotID lowestValueSlot;
int lowestValue = std::numeric_limits<int>::max();
for (auto slot : town->getUpperArmy()->Slots())
{
if (slot.second->getCreatureID() != CreatureID::NONE)
{
int currentStackMarketValue =
slot.second->getCreatureID().toCreature()->getFullRecruitCost().marketValue() * slot.second->getCount();
if (slot.second->getCreatureID().toCreature()->getFactionID() == town->getFactionID())
continue;
if (currentStackMarketValue < lowestValue)
{
lowestValue = currentStackMarketValue;
lowestValueSlot = slot.first;
}
}
}
if (lowestValueSlot.validSlot())
{
cb->dismissCreature(town->getUpperArmy(), lowestValueSlot);
}
}
if (town->getUpperArmy()->stacksCount() < GameConstants::ARMY_SIZE || town->getUpperArmy()->getSlotFor(ci.creID).validSlot()) //It is possible we don't scrap despite we wanted to due to not scrapping stacks that fit our faction
{
cb->recruitCreatures(town, town->getUpperArmy(), ci.creID, ci.count, ci.level);
}
valueBought += ci.count * ci.creID.toCreature()->getAIValue();
}
}

View File

@ -37,12 +37,6 @@ namespace Goals
{
return new T(static_cast<T const &>(*this)); //casting enforces template instantiation
}
template<typename Handler> void serialize(Handler & h)
{
h & static_cast<AbstractGoal &>(*this);
//h & goalType & isElementar & isAbstract & priority;
//h & value & resID & objid & aid & tile & hero & town & bid;
}
bool operator==(const AbstractGoal & g) const override
{

View File

@ -31,7 +31,7 @@ namespace Goals
{
objid = obj->id.getNum();
tile = obj->visitablePos();
name = obj->typeName;
name = obj->getTypeName();
}
bool operator==(const CaptureObject & other) const override;

View File

@ -12,7 +12,7 @@
#include "../Behaviors/CaptureObjectsBehavior.h"
#include "../AIGateway.h"
#include "../../../lib/VCMI_Lib.h"
#include "../../../lib/CGeneralTextHandler.h"
#include "../../../lib/texts/CGeneralTextHandler.h"
namespace NKAI
{

View File

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

View File

@ -22,11 +22,17 @@ ExecuteHeroChain::ExecuteHeroChain(const AIPath & path, const CGObjectInstance *
{
hero = path.targetHero;
tile = path.targetTile();
closestWayRatio = 1;
if(obj)
{
objid = obj->id.getNum();
targetName = obj->typeName + tile.toString();
#if NKAI_TRACE_LEVEL >= 1
targetName = obj->getObjectName() + tile.toString();
#else
targetName = obj->getTypeName() + tile.toString();
#endif
}
else
{
@ -80,6 +86,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
ai->nullkiller->setActive(chainPath.targetHero, tile);
ai->nullkiller->setTargetObject(objid);
ai->nullkiller->objectClusterizer->reset();
auto targetObject = ai->myCb->getObj(static_cast<ObjectInstanceID>(objid), false);
@ -191,6 +198,26 @@ void ExecuteHeroChain::accept(AIGateway * ai)
}
}
auto findWhirlpool = [&ai](const int3 & pos) -> ObjectInstanceID
{
auto objs = ai->myCb->getVisitableObjs(pos);
auto whirlpool = std::find_if(objs.begin(), objs.end(), [](const CGObjectInstance * o)->bool
{
return o->ID == Obj::WHIRLPOOL;
});
return whirlpool != objs.end() ? dynamic_cast<const CGWhirlpool *>(*whirlpool)->id : ObjectInstanceID(-1);
};
auto sourceWhirlpool = findWhirlpool(hero->visitablePos());
auto targetWhirlpool = findWhirlpool(node->coord);
if(i != chainPath.nodes.size() - 1 && sourceWhirlpool.hasValue() && sourceWhirlpool == targetWhirlpool)
{
logAi->trace("AI exited whirlpool at %s but expected at %s", hero->visitablePos().toString(), node->coord.toString());
continue;
}
if(hero->movementPointsRemaining())
{
try
@ -234,7 +261,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
if(node->turns == 0)
{
logAi->error(
"Unable to complete chain. Expected hero %s to arive to %s but he is at %s",
"Unable to complete chain. Expected hero %s to arrive to %s but he is at %s",
hero->getNameTranslated(),
node->coord.toString(),
hero->visitablePos().toString());
@ -260,7 +287,11 @@ void ExecuteHeroChain::accept(AIGateway * ai)
std::string ExecuteHeroChain::toString() const
{
#if NKAI_TRACE_LEVEL >= 1
return "ExecuteHeroChain " + targetName + " by " + chainPath.toString();
#else
return "ExecuteHeroChain " + targetName + " by " + chainPath.targetHero->getNameTranslated();
#endif
}
bool ExecuteHeroChain::moveHeroToTile(AIGateway * ai, const CGHeroInstance * hero, const int3 & tile)

View File

@ -59,7 +59,7 @@ void ExploreNeighbourTile::accept(AIGateway * ai)
return;
}
auto danger = ai->nullkiller->pathfinder->getStorage()->evaluateDanger(target, hero, true);
auto danger = ai->nullkiller->dangerEvaluator->evaluateDanger(target, hero, true);
if(danger > 0 || !ai->moveHeroToTile(target, hero))
{

View File

@ -68,10 +68,13 @@ void RecruitHero::accept(AIGateway * ai)
throw cannotFulfillGoalException("Town " + t->nodeName() + " is occupied. Cannot recruit hero!");
cb->recruitHero(t, heroToHire);
ai->nullkiller->heroManager->update();
if(t->visitingHero)
ai->moveHeroToTile(t->visitablePos(), t->visitingHero.get());
{
std::unique_lock lockGuard(ai->nullkiller->aiStateMutex);
ai->nullkiller->heroManager->update();
ai->nullkiller->objectClusterizer->reset();
}
}
}

View File

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

View File

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

View File

@ -14,27 +14,37 @@
namespace NKAI
{
void ArmyFormation::rearrangeArmyForSiege(const CGTownInstance * town, const CGHeroInstance * attacker)
void ArmyFormation::rearrangeArmyForWhirlpool(const CGHeroInstance * hero)
{
auto freeSlots = attacker->getFreeSlotsQueue();
addSingleCreatureStacks(hero);
}
void ArmyFormation::addSingleCreatureStacks(const CGHeroInstance * hero)
{
auto freeSlots = hero->getFreeSlotsQueue();
while(!freeSlots.empty())
{
auto weakestCreature = vstd::minElementByFun(attacker->Slots(), [](const std::pair<SlotID, CStackInstance *> & slot) -> int
auto weakestCreature = vstd::minElementByFun(hero->Slots(), [](const std::pair<SlotID, CStackInstance *> & slot) -> int
{
return slot.second->getCount() == 1
? std::numeric_limits<int>::max()
: slot.second->getCreatureID().toCreature()->getAIValue();
});
if(weakestCreature == attacker->Slots().end() || weakestCreature->second->getCount() == 1)
if(weakestCreature == hero->Slots().end() || weakestCreature->second->getCount() == 1)
{
break;
}
cb->splitStack(attacker, attacker, weakestCreature->first, freeSlots.front(), 1);
cb->splitStack(hero, hero, weakestCreature->first, freeSlots.front(), 1);
freeSlots.pop();
}
}
void ArmyFormation::rearrangeArmyForSiege(const CGTownInstance * town, const CGHeroInstance * attacker)
{
addSingleCreatureStacks(attacker);
if(town->fortLevel() > CGTownInstance::FORT)
{

View File

@ -13,8 +13,6 @@
#include "../../../lib/GameConstants.h"
#include "../../../lib/VCMI_Lib.h"
#include "../../../lib/CTownHandler.h"
#include "../../../lib/CBuildingHandler.h"
namespace NKAI
{
@ -33,6 +31,10 @@ public:
ArmyFormation(std::shared_ptr<CCallback> CB, const Nullkiller * ai): cb(CB) {}
void rearrangeArmyForSiege(const CGTownInstance * town, const CGHeroInstance * attacker);
void rearrangeArmyForWhirlpool(const CGHeroInstance * hero);
void addSingleCreatureStacks(const CGHeroInstance * hero);
};
}

View File

@ -49,7 +49,7 @@ bool ExplorationHelper::scanSector(int scanRadius)
{
int3 tile = int3(0, 0, ourPos.z);
const auto & slice = (*(ts->fogOfWarMap))[ourPos.z];
const auto & slice = ts->fogOfWarMap[ourPos.z];
for(tile.x = ourPos.x - scanRadius; tile.x <= ourPos.x + scanRadius; tile.x++)
{
@ -75,13 +75,13 @@ bool ExplorationHelper::scanMap()
foreach_tile_pos([&](const int3 & pos)
{
if((*(ts->fogOfWarMap))[pos.z][pos.x][pos.y])
if(ts->fogOfWarMap[pos.z][pos.x][pos.y])
{
bool hasInvisibleNeighbor = false;
foreach_neighbour(cbp, pos, [&](CCallback * cbp, int3 neighbour)
{
if(!(*(ts->fogOfWarMap))[neighbour.z][neighbour.x][neighbour.y])
if(!ts->fogOfWarMap[neighbour.z][neighbour.x][neighbour.y])
{
hasInvisibleNeighbor = true;
}
@ -107,7 +107,7 @@ bool ExplorationHelper::scanMap()
allowDeadEndCancellation = false;
logAi->debug("Exploration scan all possible tiles for hero %s", hero->getNameTranslated());
boost::multi_array<ui8, 3> potentialTiles = *ts->fogOfWarMap;
boost::multi_array<ui8, 3> potentialTiles = ts->fogOfWarMap;
std::vector<int3> tilesToExploreFrom = edgeTiles;
// WARNING: POTENTIAL BUG
@ -175,7 +175,7 @@ void ExplorationHelper::scanTile(const int3 & tile)
continue;
}
if(isSafeToVisit(hero, path.heroArmy, path.getTotalDanger()))
if(isSafeToVisit(hero, path.heroArmy, path.getTotalDanger(), ai->settings->getSafeAttackRatio()))
{
bestGoal = goal;
bestValue = ourValue;
@ -191,7 +191,7 @@ int ExplorationHelper::howManyTilesWillBeDiscovered(const int3 & pos) const
int ret = 0;
int3 npos = int3(0, 0, pos.z);
const auto & slice = (*(ts->fogOfWarMap))[pos.z];
const auto & slice = ts->fogOfWarMap[pos.z];
for(npos.x = pos.x - sightRadius; npos.x <= pos.x + sightRadius; npos.x++)
{

View File

@ -13,8 +13,6 @@
#include "../../../lib/GameConstants.h"
#include "../../../lib/VCMI_Lib.h"
#include "../../../lib/CTownHandler.h"
#include "../../../lib/CBuildingHandler.h"
#include "../Goals/AbstractGoal.h"
namespace NKAI

View File

@ -10,6 +10,7 @@
#include "StdInc.h"
#include "AINodeStorage.h"
#include "Actions/TownPortalAction.h"
#include "Actions/WhirlpoolAction.h"
#include "../Goals/Goals.h"
#include "../AIGateway.h"
#include "../Engine/Nullkiller.h"
@ -25,10 +26,10 @@ namespace NKAI
{
std::shared_ptr<boost::multi_array<AIPathNode, 4>> AISharedStorage::shared;
uint64_t AISharedStorage::version = 0;
uint32_t AISharedStorage::version = 0;
boost::mutex AISharedStorage::locker;
std::set<int3> commitedTiles;
std::set<int3> commitedTilesInitial;
std::set<int3> committedTiles;
std::set<int3> committedTilesInitial;
const uint64_t FirstActorMask = 1;
@ -36,19 +37,19 @@ const uint64_t MIN_ARMY_STRENGTH_FOR_CHAIN = 5000;
const uint64_t MIN_ARMY_STRENGTH_FOR_NEXT_ACTOR = 1000;
const uint64_t CHAIN_MAX_DEPTH = 4;
const bool DO_NOT_SAVE_TO_COMMITED_TILES = false;
const bool DO_NOT_SAVE_TO_COMMITTED_TILES = false;
AISharedStorage::AISharedStorage(int3 sizes)
AISharedStorage::AISharedStorage(int3 sizes, int numChains)
{
if(!shared){
shared.reset(new boost::multi_array<AIPathNode, 4>(
boost::extents[sizes.z][sizes.x][sizes.y][AIPathfinding::NUM_CHAINS]));
boost::extents[sizes.z][sizes.x][sizes.y][numChains]));
nodes = shared;
foreach_tile_pos([&](const int3 & pos)
{
for(auto i = 0; i < AIPathfinding::NUM_CHAINS; i++)
for(auto i = 0; i < numChains; i++)
{
auto & node = get(pos)[i];
@ -91,13 +92,21 @@ void AIPathNode::addSpecialAction(std::shared_ptr<const SpecialAction> action)
}
}
AINodeStorage::AINodeStorage(const Nullkiller * ai, const int3 & Sizes)
: sizes(Sizes), ai(ai), cb(ai->cb.get()), nodes(Sizes)
int AINodeStorage::getBucketCount() const
{
accesibility = std::make_unique<boost::multi_array<EPathAccessibility, 4>>(
boost::extents[sizes.z][sizes.x][sizes.y][EPathfindingLayer::NUM_LAYERS]);
return ai->settings->getPathfinderBucketsCount();
}
dangerEvaluator.reset(new FuzzyHelper(ai));
int AINodeStorage::getBucketSize() const
{
return ai->settings->getPathfinderBucketSize();
}
AINodeStorage::AINodeStorage(const Nullkiller * ai, const int3 & Sizes)
: sizes(Sizes), ai(ai), cb(ai->cb.get()), nodes(Sizes, ai->settings->getPathfinderBucketSize() * ai->settings->getPathfinderBucketsCount())
{
accessibility = std::make_unique<boost::multi_array<EPathAccessibility, 4>>(
boost::extents[sizes.z][sizes.x][sizes.y][EPathfindingLayer::NUM_LAYERS]);
}
AINodeStorage::~AINodeStorage() = default;
@ -131,10 +140,10 @@ void AINodeStorage::initialize(const PathfinderOptions & options, const CGameSta
for(pos.y = 0; pos.y < sizes.y; ++pos.y)
{
const TerrainTile & tile = gs->map->getTile(pos);
if (!tile.terType->isPassable())
if (!tile.getTerrain()->isPassable())
continue;
if (tile.terType->isWater())
if (tile.isWater())
{
resetTile(pos, ELayer::SAIL, PathfinderUtil::evaluateAccessibility<ELayer::SAIL>(pos, tile, fow, player, gs));
if (useFlying)
@ -157,7 +166,7 @@ void AINodeStorage::initialize(const PathfinderOptions & options, const CGameSta
void AINodeStorage::clear()
{
actors.clear();
commitedTiles.clear();
committedTiles.clear();
heroChainPass = EHeroChainPass::INITIAL;
heroChainTurn = 0;
heroChainMaxTurns = 1;
@ -170,8 +179,8 @@ std::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
const EPathfindingLayer layer,
const ChainActor * actor)
{
int bucketIndex = ((uintptr_t)actor + static_cast<uint32_t>(layer)) % AIPathfinding::BUCKET_COUNT;
int bucketOffset = bucketIndex * AIPathfinding::BUCKET_SIZE;
int bucketIndex = ((uintptr_t)actor + static_cast<uint32_t>(layer)) % ai->settings->getPathfinderBucketsCount();
int bucketOffset = bucketIndex * ai->settings->getPathfinderBucketSize();
auto chains = nodes.get(pos);
if(blocked(pos, layer))
@ -179,7 +188,7 @@ std::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
return std::nullopt;
}
for(auto i = AIPathfinding::BUCKET_SIZE - 1; i >= 0; i--)
for(auto i = ai->settings->getPathfinderBucketSize() - 1; i >= 0; i--)
{
AIPathNode & node = chains[i + bucketOffset];
@ -224,7 +233,6 @@ std::vector<CGPathNode *> AINodeStorage::getInitialNodes()
AIPathNode * initialNode = allocated.value();
initialNode->inPQ = false;
initialNode->pq = nullptr;
initialNode->turns = actor->initialTurn;
initialNode->moveRemains = actor->initialMovement;
@ -256,10 +264,45 @@ void AINodeStorage::commit(CDestinationNodeInfo & destination, const PathNodeInf
{
commit(dstNode, srcNode, destination.action, destination.turn, destination.movementLeft, destination.cost);
if(srcNode->specialAction || srcNode->chainOther)
// regular pathfinder can not go directly through whirlpool
bool isWhirlpoolTeleport = destination.nodeObject
&& destination.nodeObject->ID == Obj::WHIRLPOOL;
if(srcNode->specialAction
|| srcNode->chainOther
|| isWhirlpoolTeleport)
{
// there is some action on source tile which should be performed before we can bypass it
destination.node->theNodeBefore = source.node;
dstNode->theNodeBefore = source.node;
if(isWhirlpoolTeleport)
{
if(dstNode->actor->creatureSet->Slots().size() == 1
&& dstNode->actor->creatureSet->Slots().begin()->second->getCount() == 1)
{
return;
}
auto weakest = vstd::minElementByFun(dstNode->actor->creatureSet->Slots(), [](std::pair<SlotID, const CStackInstance *> pair) -> int
{
return pair.second->getCount() * pair.second->getCreatureID().toCreature()->getAIValue();
});
if(weakest == dstNode->actor->creatureSet->Slots().end())
{
logAi->debug("Empty army entering whirlpool detected at tile %s", dstNode->coord.toString());
destination.blocked = true;
return;
}
if(dstNode->actor->creatureSet->getFreeSlots().size())
dstNode->armyLoss += weakest->second->getCreatureID().toCreature()->getAIValue();
else
dstNode->armyLoss += (weakest->second->getCount() + 1) / 2 * weakest->second->getCreatureID().toCreature()->getAIValue();
dstNode->specialAction = AIPathfinding::WhirlpoolAction::instance;
}
}
if(dstNode->specialAction && dstNode->actor)
@ -276,7 +319,7 @@ void AINodeStorage::commit(
int turn,
int movementLeft,
float cost,
bool saveToCommited) const
bool saveToCommitted) const
{
destination->action = action;
destination->setCost(cost);
@ -290,7 +333,7 @@ void AINodeStorage::commit(
#if NKAI_PATHFINDER_TRACE_LEVEL >= 2
logAi->trace(
"Commited %s -> %s, layer: %d, cost: %f, turn: %s, mp: %d, hero: %s, mask: %x, army: %lld",
"Committed %s -> %s, layer: %d, cost: %f, turn: %s, mp: %d, hero: %s, mask: %x, army: %lld",
source->coord.toString(),
destination->coord.toString(),
destination->layer,
@ -302,9 +345,9 @@ void AINodeStorage::commit(
destination->actor->armyValue);
#endif
if(saveToCommited && destination->turns <= heroChainTurn)
if(saveToCommitted && destination->turns <= heroChainTurn)
{
commitedTiles.insert(destination->coord);
committedTiles.insert(destination->coord);
}
if(destination->turns == source->turns)
@ -374,7 +417,7 @@ bool AINodeStorage::increaseHeroChainTurnLimit()
return false;
heroChainTurn++;
commitedTiles.clear();
committedTiles.clear();
for(auto layer : phisycalLayers)
{
@ -384,7 +427,7 @@ bool AINodeStorage::increaseHeroChainTurnLimit()
{
if(node.turns <= heroChainTurn && node.action != EPathNodeAction::UNKNOWN)
{
commitedTiles.insert(pos);
committedTiles.insert(pos);
return true;
}
@ -453,8 +496,8 @@ public:
AINodeStorage & storage, const std::vector<int3> & tiles, uint64_t chainMask, int heroChainTurn)
:existingChains(), newChains(), delayedWork(), storage(storage), chainMask(chainMask), heroChainTurn(heroChainTurn), heroChain(), tiles(tiles)
{
existingChains.reserve(AIPathfinding::NUM_CHAINS);
newChains.reserve(AIPathfinding::NUM_CHAINS);
existingChains.reserve(storage.getBucketCount() * storage.getBucketSize());
newChains.reserve(storage.getBucketCount() * storage.getBucketSize());
}
void execute(const tbb::blocked_range<size_t>& r)
@ -545,7 +588,7 @@ bool AINodeStorage::calculateHeroChain()
heroChainPass = EHeroChainPass::CHAIN;
heroChain.clear();
std::vector<int3> data(commitedTiles.begin(), commitedTiles.end());
std::vector<int3> data(committedTiles.begin(), committedTiles.end());
if(data.size() > 100)
{
@ -576,7 +619,7 @@ bool AINodeStorage::calculateHeroChain()
task.flushResult(heroChain);
}
commitedTiles.clear();
committedTiles.clear();
return !heroChain.empty();
}
@ -592,7 +635,7 @@ bool AINodeStorage::selectFirstActor()
});
chainMask = strongest->chainMask;
commitedTilesInitial = commitedTiles;
committedTilesInitial = committedTiles;
return true;
}
@ -627,7 +670,7 @@ bool AINodeStorage::selectNextActor()
return false;
chainMask = nextActor->get()->chainMask;
commitedTiles = commitedTilesInitial;
committedTiles = committedTilesInitial;
return true;
}
@ -654,7 +697,7 @@ void HeroChainCalculationTask::cleanupInefectiveChains(std::vector<ExchangeCandi
if(isNotEffective)
{
logAi->trace(
"Skip exchange %s[%x] -> %s[%x] at %s is ineficient",
"Skip exchange %s[%x] -> %s[%x] at %s is inefficient",
chainInfo.otherParent->actor->toString(),
chainInfo.otherParent->actor->chainMask,
chainInfo.carrierParent->actor->toString(),
@ -686,6 +729,7 @@ void HeroChainCalculationTask::calculateHeroChain(
if(node->action == EPathNodeAction::BATTLE
|| node->action == EPathNodeAction::TELEPORT_BATTLE
|| node->action == EPathNodeAction::TELEPORT_NORMAL
|| node->action == EPathNodeAction::DISEMBARK
|| node->action == EPathNodeAction::TELEPORT_BLOCKING_VISIT)
{
continue;
@ -754,7 +798,7 @@ void HeroChainCalculationTask::calculateHeroChain(
if(hasLessMp && hasLessExperience)
{
#if NKAI_PATHFINDER_TRACE_LEVEL >= 2
logAi->trace("Exchange at %s is ineficient. Blocked.", carrier->coord.toString());
logAi->trace("Exchange at %s is inefficient. Blocked.", carrier->coord.toString());
#endif
return;
}
@ -823,7 +867,7 @@ void HeroChainCalculationTask::addHeroChain(const std::vector<ExchangeCandidate>
chainInfo.turns,
chainInfo.moveRemains,
chainInfo.getCost(),
DO_NOT_SAVE_TO_COMMITED_TILES);
DO_NOT_SAVE_TO_COMMITTED_TILES);
if(carrier->specialAction || carrier->chainOther)
{
@ -928,7 +972,7 @@ void AINodeStorage::setHeroes(std::map<const CGHeroInstance *, HeroRole> heroes)
// do not allow our own heroes in garrison to act on map
if(hero.first->getOwner() == ai->playerID
&& hero.first->inTownGarrison
&& (ai->isHeroLocked(hero.first) || ai->heroManager->heroCapReached()))
&& (ai->isHeroLocked(hero.first) || ai->heroManager->heroCapReached(false)))
{
continue;
}
@ -1015,8 +1059,8 @@ std::vector<CGPathNode *> AINodeStorage::calculateTeleportations(
for(auto & neighbour : accessibleExits)
{
auto node = getOrCreateNode(neighbour, source.node->layer, srcNode->actor);
std::optional<AIPathNode *> node = getOrCreateNode(neighbour, source.node->layer, srcNode->actor);
if(!node)
continue;
@ -1027,7 +1071,7 @@ std::vector<CGPathNode *> AINodeStorage::calculateTeleportations(
return neighbours;
}
struct TowmPortalFinder
struct TownPortalFinder
{
const std::vector<CGPathNode *> & initialNodes;
MasteryLevel::Type townPortalSkillLevel;
@ -1040,7 +1084,7 @@ struct TowmPortalFinder
SpellID spellID;
const CSpell * townPortal;
TowmPortalFinder(
TownPortalFinder(
const ChainActor * actor,
const std::vector<CGPathNode *> & initialNodes,
std::vector<const CGTownInstance *> targetTowns,
@ -1117,7 +1161,7 @@ struct TowmPortalFinder
bestNode->turns,
bestNode->moveRemains - movementNeeded,
movementCost,
DO_NOT_SAVE_TO_COMMITED_TILES);
DO_NOT_SAVE_TO_COMMITTED_TILES);
node->theNodeBefore = bestNode;
node->addSpecialAction(std::make_shared<AIPathfinding::TownPortalAction>(targetTown));
@ -1146,7 +1190,7 @@ void AINodeStorage::calculateTownPortal(
return; // no towns no need to run loop further
}
TowmPortalFinder townPortalFinder(actor, initialNodes, towns, this);
TownPortalFinder townPortalFinder(actor, initialNodes, towns, this);
if(townPortalFinder.actorCanCastTownPortal())
{
@ -1163,6 +1207,11 @@ void AINodeStorage::calculateTownPortal(
continue;
}
if (targetTown->visitingHero
&& (targetTown->visitingHero.get()->getFactionID() != actor->hero->getFactionID()
|| targetTown->getUpperArmy()->stacksCount()))
continue;
auto nodeOptional = townPortalFinder.createTownPortalNode(targetTown);
if(nodeOptional)
@ -1279,7 +1328,7 @@ bool AINodeStorage::isOtherChainBetter(
{
#if NKAI_PATHFINDER_TRACE_LEVEL >= 2
logAi->trace(
"Block ineficient battle move %s->%s, hero: %s[%X], army %lld, mp diff: %i",
"Block inefficient battle move %s->%s, hero: %s[%X], army %lld, mp diff: %i",
source->coord.toString(),
candidateNode.coord.toString(),
candidateNode.actor->hero->getNameTranslated(),
@ -1303,7 +1352,7 @@ bool AINodeStorage::isOtherChainBetter(
{
#if NKAI_PATHFINDER_TRACE_LEVEL >= 2
logAi->trace(
"Block ineficient move because of stronger army %s->%s, hero: %s[%X], army %lld, mp diff: %i",
"Block inefficient move because of stronger army %s->%s, hero: %s[%X], army %lld, mp diff: %i",
source->coord.toString(),
candidateNode.coord.toString(),
candidateNode.actor->hero->getNameTranslated(),
@ -1329,7 +1378,7 @@ bool AINodeStorage::isOtherChainBetter(
#if NKAI_PATHFINDER_TRACE_LEVEL >= 2
logAi->trace(
"Block ineficient move because of stronger hero %s->%s, hero: %s[%X], army %lld, mp diff: %i",
"Block inefficient move because of stronger hero %s->%s, hero: %s[%X], army %lld, mp diff: %i",
source->coord.toString(),
candidateNode.coord.toString(),
candidateNode.actor->hero->getNameTranslated(),
@ -1384,7 +1433,33 @@ void AINodeStorage::calculateChainInfo(std::vector<AIPath> & paths, const int3 &
path.targetHero = node.actor->hero;
path.heroArmy = node.actor->creatureSet;
path.armyLoss = node.armyLoss;
path.targetObjectDanger = evaluateDanger(pos, path.targetHero, !node.actor->allowBattle);
path.targetObjectDanger = ai->dangerEvaluator->evaluateDanger(pos, path.targetHero, !node.actor->allowBattle);
for (auto pathNode : path.nodes)
{
path.targetObjectDanger = std::max(ai->dangerEvaluator->evaluateDanger(pathNode.coord, path.targetHero, !node.actor->allowBattle), path.targetObjectDanger);
}
if(path.targetObjectDanger > 0)
{
if(node.theNodeBefore)
{
auto prevNode = getAINode(node.theNodeBefore);
if(node.coord == prevNode->coord && node.actor->hero == prevNode->actor->hero)
{
paths.pop_back();
continue;
}
else
{
path.armyLoss = prevNode->armyLoss;
}
}
else
{
path.armyLoss = 0;
}
}
path.targetObjectArmyLoss = evaluateArmyLoss(
path.targetHero,
@ -1509,7 +1584,7 @@ uint8_t AIPath::turn() const
uint64_t AIPath::getHeroStrength() const
{
return targetHero->getFightingStrength() * getHeroArmyStrengthWithCommander(targetHero, heroArmy);
return targetHero->getHeroStrength() * getHeroArmyStrengthWithCommander(targetHero, heroArmy);
}
uint64_t AIPath::getTotalDanger() const

View File

@ -29,9 +29,6 @@ namespace NKAI
{
namespace AIPathfinding
{
const int BUCKET_COUNT = 3;
const int BUCKET_SIZE = 7;
const int NUM_CHAINS = BUCKET_COUNT * BUCKET_SIZE;
const int CHAIN_MAX_DEPTH = 4;
}
@ -44,14 +41,17 @@ enum DayFlags : ui8
struct AIPathNode : public CGPathNode
{
std::shared_ptr<const SpecialAction> specialAction;
const AIPathNode * chainOther;
const ChainActor * actor;
uint64_t danger;
uint64_t armyLoss;
uint32_t version;
int16_t manaCost;
DayFlags dayFlags;
const AIPathNode * chainOther;
std::shared_ptr<const SpecialAction> specialAction;
const ChainActor * actor;
uint64_t version;
void addSpecialAction(std::shared_ptr<const SpecialAction> action);
@ -152,9 +152,9 @@ class AISharedStorage
std::shared_ptr<boost::multi_array<AIPathNode, 4>> nodes;
public:
static boost::mutex locker;
static uint64_t version;
static uint32_t version;
AISharedStorage(int3 mapSize);
AISharedStorage(int3 sizes, int numChains);
~AISharedStorage();
STRONG_INLINE
@ -169,11 +169,10 @@ class AINodeStorage : public INodeStorage
private:
int3 sizes;
std::unique_ptr<boost::multi_array<EPathAccessibility, 4>> accesibility;
std::unique_ptr<boost::multi_array<EPathAccessibility, 4>> accessibility;
const CPlayerSpecificInfoCallback * cb;
const Nullkiller * ai;
std::unique_ptr<FuzzyHelper> dangerEvaluator;
AISharedStorage nodes;
std::vector<std::shared_ptr<ChainActor>> actors;
std::vector<CGPathNode *> heroChain;
@ -195,6 +194,9 @@ public:
bool selectFirstActor();
bool selectNextActor();
int getBucketCount() const;
int getBucketSize() const;
std::vector<CGPathNode *> getInitialNodes() override;
virtual void calculateNeighbours(
@ -218,7 +220,7 @@ public:
int turn,
int movementLeft,
float cost,
bool saveToCommited = true) const;
bool saveToCommitted = true) const;
inline const AIPathNode * getAINode(const CGPathNode * node) const
{
@ -261,7 +263,7 @@ public:
const AIPathNode & candidateNode,
const AIPathNode & other) const;
bool isMovementIneficient(const PathNodeInfo & source, CDestinationNodeInfo & destination) const
bool isMovementInefficient(const PathNodeInfo & source, CDestinationNodeInfo & destination) const
{
return hasBetterChain(source, destination);
}
@ -282,26 +284,21 @@ public:
bool calculateHeroChain();
bool calculateHeroChainFinal();
inline uint64_t evaluateDanger(const int3 & tile, const CGHeroInstance * hero, bool checkGuards) const
{
return dangerEvaluator->evaluateDanger(tile, hero, checkGuards);
}
uint64_t evaluateArmyLoss(const CGHeroInstance * hero, uint64_t armyValue, uint64_t danger) const;
inline EPathAccessibility getAccessibility(const int3 & tile, EPathfindingLayer layer) const
{
return (*this->accesibility)[tile.z][tile.x][tile.y][layer];
return (*this->accessibility)[tile.z][tile.x][tile.y][layer];
}
inline void resetTile(const int3 & tile, EPathfindingLayer layer, EPathAccessibility tileAccessibility)
{
(*this->accesibility)[tile.z][tile.x][tile.y][layer] = tileAccessibility;
(*this->accessibility)[tile.z][tile.x][tile.y][layer] = tileAccessibility;
}
inline int getBucket(const ChainActor * actor) const
{
return ((uintptr_t)actor * 395) % AIPathfinding::BUCKET_COUNT;
return ((uintptr_t)actor * 395) % getBucketCount();
}
void calculateTownPortalTeleportations(std::vector<CGPathNode *> & neighbours);

View File

@ -13,7 +13,7 @@
#include "Rules/AIMovementAfterDestinationRule.h"
#include "Rules/AIMovementToDestinationRule.h"
#include "Rules/AIPreviousNodeRule.h"
#include "../Engine//Nullkiller.h"
#include "../Engine/Nullkiller.h"
#include "../../../lib/pathfinder/CPathfinder.h"
@ -44,10 +44,12 @@ namespace AIPathfinding
Nullkiller * ai,
std::shared_ptr<AINodeStorage> nodeStorage,
bool allowBypassObjects)
:PathfinderConfig(nodeStorage, makeRuleset(cb, ai, nodeStorage, allowBypassObjects)), aiNodeStorage(nodeStorage)
:PathfinderConfig(nodeStorage, cb, makeRuleset(cb, ai, nodeStorage, allowBypassObjects)), aiNodeStorage(nodeStorage)
{
options.canUseCast = true;
options.allowLayerTransitioningAfterBattle = true;
options.useTeleportWhirlpool = true;
options.forceUseTeleportWhirlpool = true;
}
AIPathfinderConfig::~AIPathfinderConfig() = default;

View File

@ -0,0 +1,55 @@
/*
* WhirlpoolAction.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 "../../Goals/AdventureSpellCast.h"
#include "../../../../lib/mapObjects/MapObjects.h"
#include "WhirlpoolAction.h"
#include "../../AIGateway.h"
namespace NKAI
{
using namespace AIPathfinding;
std::shared_ptr<WhirlpoolAction> WhirlpoolAction::instance = std::make_shared<WhirlpoolAction>();
void WhirlpoolAction::execute(AIGateway * ai, const CGHeroInstance * hero) const
{
ai->nullkiller->armyFormation->rearrangeArmyForWhirlpool(hero);
}
std::string WhirlpoolAction::toString() const
{
return "Prepare for whirlpool";
}
/*
bool TownPortalAction::canAct(const CGHeroInstance * hero, const AIPathNode * source) const
{
#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 + getManaCost(hero);
}
uint32_t TownPortalAction::getManaCost(const CGHeroInstance * hero) const
{
SpellID summonBoat = SpellID::TOWN_PORTAL;
return hero->getSpellCost(summonBoat.toSpell());
}*/
}

View File

@ -0,0 +1,35 @@
/*
* WhirlpoolAction.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"
#include "../../Goals/AdventureSpellCast.h"
namespace NKAI
{
namespace AIPathfinding
{
class WhirlpoolAction : public SpecialAction
{
public:
WhirlpoolAction()
{
}
static std::shared_ptr<WhirlpoolAction> instance;
void execute(AIGateway * ai, const CGHeroInstance * hero) const override;
std::string toString() const override;
};
}
}

View File

@ -46,7 +46,7 @@ ChainActor::ChainActor(const CGHeroInstance * hero, HeroRole heroRole, uint64_t
initialMovement = hero->movementPointsRemaining();
initialTurn = 0;
armyValue = getHeroArmyStrengthWithCommander(hero, hero);
heroFightingStrength = hero->getFightingStrength();
heroFightingStrength = hero->getHeroStrength();
tiCache.reset(new TurnInfo(hero));
}
@ -182,7 +182,7 @@ ExchangeResult HeroActor::tryExchangeNoLock(const ChainActor * specialActor, con
return &actor == specialActor;
});
result.actor = &(dynamic_cast<HeroActor *>(result.actor)->specialActors[index]);
result.actor = &(dynamic_cast<HeroActor *>(result.actor)->specialActors.at(index));
return result;
}
@ -217,7 +217,7 @@ ExchangeResult HeroExchangeMap::tryExchangeNoLock(const ChainActor * other)
ExchangeResult result;
{
boost::shared_lock<boost::shared_mutex> lock(sync, boost::try_to_lock);
boost::shared_lock lock(sync, boost::try_to_lock);
if(!lock.owns_lock())
{
@ -237,7 +237,7 @@ ExchangeResult HeroExchangeMap::tryExchangeNoLock(const ChainActor * other)
}
{
boost::unique_lock<boost::shared_mutex> uniqueLock(sync, boost::try_to_lock);
boost::unique_lock uniqueLock(sync, boost::try_to_lock);
if(!uniqueLock.owns_lock())
{
@ -374,10 +374,12 @@ HeroExchangeArmy * HeroExchangeMap::tryUpgrade(
for(auto & creatureToBuy : buyArmy)
{
auto targetSlot = target->getSlotFor(creatureToBuy.creID.toCreature());
target->addToSlot(targetSlot, creatureToBuy.creID, creatureToBuy.count);
target->armyCost += creatureToBuy.creID.toCreature()->getFullRecruitCost() * creatureToBuy.count;
target->requireBuyArmy = true;
if (targetSlot.validSlot())
{
target->addToSlot(targetSlot, creatureToBuy.creID, creatureToBuy.count);
target->armyCost += creatureToBuy.creID.toCreature()->getFullRecruitCost() * creatureToBuy.count;
target->requireBuyArmy = true;
}
}
}
@ -440,7 +442,7 @@ int DwellingActor::getInitialTurn(bool waitForGrowth, int dayOfWeek)
std::string DwellingActor::toString() const
{
return dwelling->typeName + dwelling->visitablePos().toString();
return dwelling->getTypeName() + dwelling->visitablePos().toString();
}
CCreatureSet * DwellingActor::getDwellingCreatures(const CGDwelling * dwelling, bool waitForGrowth)

View File

@ -113,7 +113,7 @@ public:
static const int SPECIAL_ACTORS_COUNT = 7;
private:
ChainActor specialActors[SPECIAL_ACTORS_COUNT];
std::array<ChainActor, SPECIAL_ACTORS_COUNT> specialActors;
std::unique_ptr<HeroExchangeMap> exchangeMap;
void setupSpecialActors();

View File

@ -160,7 +160,7 @@ void GraphPaths::dumpToLog() const
node.previous.coord.toString(),
tile.first.toString(),
node.cost,
node.danger);
node.linkDanger);
}
logBuilder.addLine(node.previous.coord, tile.first);
@ -169,14 +169,17 @@ void GraphPaths::dumpToLog() const
});
}
bool GraphPathNode::tryUpdate(const GraphPathNodePointer & pos, const GraphPathNode & prev, const ObjectLink & link)
bool GraphPathNode::tryUpdate(
const GraphPathNodePointer & pos,
const GraphPathNode & prev,
const ObjectLink & link)
{
auto newCost = prev.cost + link.cost;
if(newCost < cost)
{
previous = pos;
danger = prev.danger + link.danger;
linkDanger = link.danger;
cost = newCost;
return true;
@ -199,7 +202,7 @@ void GraphPaths::addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHe
std::vector<GraphPathNodePointer> tilesToPass;
uint64_t danger = node.danger;
uint64_t danger = node.linkDanger;
float cost = node.cost;
bool allowBattle = false;
@ -212,13 +215,13 @@ void GraphPaths::addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHe
if(currentTile == pathNodes.end())
break;
auto currentNode = currentTile->second[current.nodeType];
auto & currentNode = currentTile->second[current.nodeType];
if(!currentNode.previous.valid())
break;
allowBattle = allowBattle || currentNode.nodeType == GrapthPathNodeType::BATTLE;
vstd::amax(danger, currentNode.danger);
vstd::amax(danger, currentNode.linkDanger);
vstd::amax(cost, currentNode.cost);
tilesToPass.push_back(current);
@ -239,9 +242,13 @@ void GraphPaths::addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHe
if(path.targetHero != hero)
continue;
for(auto graphTile = tilesToPass.rbegin(); graphTile != tilesToPass.rend(); graphTile++)
uint64_t loss = 0;
uint64_t strength = getHeroArmyStrengthWithCommander(path.targetHero, path.heroArmy);
for(auto graphTile = ++tilesToPass.rbegin(); graphTile != tilesToPass.rend(); graphTile++)
{
AIPathNodeInfo n;
auto & node = getNode(*graphTile);
n.coord = graphTile->coord;
n.cost = cost;
@ -249,7 +256,21 @@ void GraphPaths::addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHe
n.danger = danger;
n.targetHero = hero;
n.parentIndex = -1;
n.specialAction = getNode(*graphTile).specialAction;
n.specialAction = node.specialAction;
if(node.linkDanger > 0)
{
auto additionalLoss = ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, strength, node.linkDanger);
loss += additionalLoss;
if(strength > additionalLoss)
strength -= additionalLoss;
else
{
strength = 0;
break;
}
}
if(n.specialAction)
{
@ -264,8 +285,13 @@ void GraphPaths::addChainInfo(std::vector<AIPath> & paths, int3 tile, const CGHe
path.nodes.insert(path.nodes.begin(), n);
}
path.armyLoss += ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), danger);
path.targetObjectDanger = ai->pathfinder->getStorage()->evaluateDanger(tile, path.targetHero, !allowBattle);
if(strength == 0)
{
continue;
}
path.armyLoss += loss;
path.targetObjectDanger = ai->dangerEvaluator->evaluateDanger(tile, path.targetHero, !allowBattle);
path.targetObjectArmyLoss = ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), path.targetObjectDanger);
paths.push_back(path);
@ -287,7 +313,7 @@ void GraphPaths::quickAddChainInfoWithBlocker(std::vector<AIPath> & paths, int3
std::vector<GraphPathNodePointer> tilesToPass;
uint64_t danger = targetNode.danger;
uint64_t danger = targetNode.linkDanger;
float cost = targetNode.cost;
bool allowBattle = false;
@ -303,7 +329,7 @@ void GraphPaths::quickAddChainInfoWithBlocker(std::vector<AIPath> & paths, int3
auto currentNode = currentTile->second[current.nodeType];
allowBattle = allowBattle || currentNode.nodeType == GrapthPathNodeType::BATTLE;
vstd::amax(danger, currentNode.danger);
vstd::amax(danger, currentNode.linkDanger);
vstd::amax(cost, currentNode.cost);
tilesToPass.push_back(current);
@ -330,7 +356,7 @@ void GraphPaths::quickAddChainInfoWithBlocker(std::vector<AIPath> & paths, int3
path.heroArmy = entryPath.heroArmy;
path.exchangeCount = entryPath.exchangeCount;
path.armyLoss = entryPath.armyLoss + ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), danger);
path.targetObjectDanger = ai->pathfinder->getStorage()->evaluateDanger(tile, path.targetHero, !allowBattle);
path.targetObjectDanger = ai->dangerEvaluator->evaluateDanger(tile, path.targetHero, !allowBattle);
path.targetObjectArmyLoss = ai->pathfinder->getStorage()->evaluateArmyLoss(path.targetHero, path.heroArmy->getArmyStrength(), path.targetObjectDanger);
AIPathNodeInfo n;
@ -341,7 +367,7 @@ void GraphPaths::quickAddChainInfoWithBlocker(std::vector<AIPath> & paths, int3
// final node
n.coord = tile;
n.cost = targetNode.cost;
n.danger = targetNode.danger;
n.danger = danger;
n.parentIndex = path.nodes.size();
path.nodes.push_back(n);
@ -368,7 +394,7 @@ void GraphPaths::quickAddChainInfoWithBlocker(std::vector<AIPath> & paths, int3
n.coord = graphTile->coord;
n.cost = node.cost;
n.turns = static_cast<ui8>(node.cost);
n.danger = node.danger;
n.danger = danger;
n.specialAction = node.specialAction;
n.parentIndex = path.nodes.size();

View File

@ -67,7 +67,7 @@ struct GraphPathNode
GrapthPathNodeType nodeType = GrapthPathNodeType::NORMAL;
GraphPathNodePointer previous;
float cost = BAD_COST;
uint64_t danger = 0;
uint64_t linkDanger = 0;
const CGObjectInstance * obj = nullptr;
std::shared_ptr<SpecialAction> specialAction;

View File

@ -164,7 +164,7 @@ void ObjectGraphCalculator::calculateConnections(const int3 & pos, std::vector<A
auto from = path.targetHero->visitablePos();
auto fromObj = actorObjectMap[path.targetHero];
auto danger = ai->pathfinder->getStorage()->evaluateDanger(pos, path.targetHero, true);
auto danger = ai->dangerEvaluator->evaluateDanger(pos, path.targetHero, true);
auto updated = target->tryAddConnection(
from,
pos,
@ -220,7 +220,7 @@ void ObjectGraphCalculator::calculateConnections(const int3 & pos, std::vector<A
continue;
}
auto danger = ai->pathfinder->getStorage()->evaluateDanger(pos2, path1.targetHero, true);
auto danger = ai->dangerEvaluator->evaluateDanger(pos2, path1.targetHero, true);
auto updated = target->tryAddConnection(
pos1,
@ -321,7 +321,7 @@ void ObjectGraphCalculator::addObjectActor(const CGObjectInstance * obj)
void ObjectGraphCalculator::addJunctionActor(const int3 & visitablePos, bool isVirtualBoat)
{
std::lock_guard<std::mutex> lock(syncLock);
std::lock_guard lock(syncLock);
auto internalCb = temporaryActorHeroes.front()->cb;
auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique<CGHeroInstance>(internalCb)).get();

View File

@ -164,7 +164,7 @@ namespace AIPathfinding
if(hero->canCastThisSpell(summonBoatSpell)
&& 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 existence of some boat
summonableVirtualBoats[hero] = std::make_shared<SummonBoatAction>();
}
}

View File

@ -11,9 +11,11 @@
#include "AIMovementAfterDestinationRule.h"
#include "../Actions/BattleAction.h"
#include "../Actions/QuestAction.h"
#include "../Actions/WhirlpoolAction.h"
#include "../../Goals/Invalid.h"
#include "AIPreviousNodeRule.h"
#include "../../../../lib/pathfinder/PathfinderOptions.h"
#include "../../../../lib/pathfinder/CPathfinder.h"
namespace NKAI
{
@ -34,7 +36,7 @@ namespace AIPathfinding
const PathfinderConfig * pathfinderConfig,
CPathfinderHelper * pathfinderHelper) const
{
if(nodeStorage->isMovementIneficient(source, destination))
if(nodeStorage->isMovementInefficient(source, destination))
{
destination.node->locked = true;
destination.blocked = true;
@ -225,7 +227,7 @@ namespace AIPathfinding
return false;
}
auto danger = nodeStorage->evaluateDanger(destination.coord, nodeStorage->getHero(destination.node), true);
auto danger = ai->dangerEvaluator->evaluateDanger(destination.coord, nodeStorage->getHero(destination.node), true);
if(danger)
{
@ -311,7 +313,7 @@ namespace AIPathfinding
}
auto hero = nodeStorage->getHero(source.node);
uint64_t danger = nodeStorage->evaluateDanger(destination.coord, hero, true);
uint64_t danger = ai->dangerEvaluator->evaluateDanger(destination.coord, hero, true);
uint64_t actualArmyValue = srcNode->actor->armyValue - srcNode->armyLoss;
uint64_t loss = nodeStorage->evaluateArmyLoss(hero, actualArmyValue, danger);

View File

@ -1,84 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<CodeBlocks_project_file>
<FileVersion major="1" minor="6" />
<Project>
<Option title="StupidAI" />
<Option pch_mode="2" />
<Option compiler="gcc" />
<Build>
<Target title="Debug-win32">
<Option platforms="Windows;" />
<Option output="../StupidAI" imp_lib="$(TARGET_OUTPUT_DIR)$(TARGET_OUTPUT_BASENAME).a" def_file="$(TARGET_OUTPUT_DIR)$(TARGET_OUTPUT_BASENAME).def" prefix_auto="1" extension_auto="1" />
<Option object_output="obj/Debug/x86" />
<Option type="3" />
<Option compiler="gcc" />
<Compiler>
<Add option="-ggdb" />
</Compiler>
<Linker>
<Add option="-lboost_system$(#boost.libsuffix32)" />
<Add option="-lVCMI_lib" />
<Add directory="$(#boost.lib32)" />
</Linker>
</Target>
<Target title="Release-win32">
<Option platforms="Windows;" />
<Option output="../StupidAI" imp_lib="$(TARGET_OUTPUT_DIR)$(TARGET_OUTPUT_BASENAME).a" def_file="$(TARGET_OUTPUT_DIR)$(TARGET_OUTPUT_BASENAME).def" prefix_auto="1" extension_auto="1" />
<Option object_output="obj/Release/x86" />
<Option type="3" />
<Option compiler="gcc" />
<Compiler>
<Add option="-fomit-frame-pointer" />
<Add option="-O3" />
</Compiler>
<Linker>
<Add option="-s" />
<Add option="-lboost_system$(#boost.libsuffix32)" />
<Add option="-lVCMI_lib" />
<Add directory="$(#boost.lib32)" />
</Linker>
</Target>
<Target title="Debug-win64">
<Option platforms="Windows;" />
<Option output="../StupidAI" imp_lib="$(TARGET_OUTPUT_DIR)$(TARGET_OUTPUT_BASENAME).a" def_file="$(TARGET_OUTPUT_DIR)$(TARGET_OUTPUT_BASENAME).def" prefix_auto="1" extension_auto="1" />
<Option object_output="obj/Debug/x64" />
<Option type="3" />
<Option compiler="gnu_gcc_compiler_x64" />
<Linker>
<Add option="-lboost_system$(#boost.libsuffix64)" />
<Add option="-lVCMI_lib" />
<Add directory="$(#boost.lib64)" />
</Linker>
</Target>
</Build>
<Compiler>
<Add option="-pedantic" />
<Add option="-Wextra" />
<Add option="-Wall" />
<Add option="-std=gnu++11" />
<Add option="-fexceptions" />
<Add option="-Wpointer-arith" />
<Add option="-Wno-switch" />
<Add option="-Wno-sign-compare" />
<Add option="-Wno-unused-parameter" />
<Add option="-Wno-overloaded-virtual" />
<Add option="-DBOOST_ALL_DYN_LINK" />
<Add option="-DBOOST_SYSTEM_NO_DEPRECATED" />
<Add option="-D_WIN32_WINNT=0x0600" />
<Add option="-D_WIN32" />
<Add directory="$(#boost.include)" />
<Add directory="../../include" />
</Compiler>
<Linker>
<Add directory="../.." />
</Linker>
<Unit filename="StdInc.h">
<Option compile="1" />
<Option weight="0" />
</Unit>
<Unit filename="StupidAI.cpp" />
<Unit filename="StupidAI.h" />
<Unit filename="main.cpp" />
<Extensions />
</Project>
</CodeBlocks_project_file>

View File

@ -18,7 +18,7 @@
#include "../../lib/CRandomGenerator.h"
CStupidAI::CStupidAI()
: side(-1)
: side(BattleSide::NONE)
, wasWaitingForRealize(false)
, wasUnlockingGs(false)
{
@ -262,7 +262,7 @@ void CStupidAI::battleStacksEffectsSet(const BattleID & battleID, const SetStack
print("battleStacksEffectsSet called");
}
void CStupidAI::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side, bool replayAllowed)
void CStupidAI::battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, BattleSide Side, bool replayAllowed)
{
print("battleStart called");
side = Side;
@ -296,7 +296,11 @@ BattleAction CStupidAI::goTowards(const BattleID & battleID, const CStack * stac
for(auto hex : hexes)
{
if(vstd::contains(avHexes, hex))
{
if(stack->position == hex)
return BattleAction::makeDefend(stack);
return BattleAction::makeMove(stack, hex);
}
if(stack->coversPos(hex))
{
@ -336,7 +340,11 @@ BattleAction CStupidAI::goTowards(const BattleID & battleID, const CStack * stac
}
if(vstd::contains(avHexes, currentDest))
{
if(stack->position == currentDest)
return BattleAction::makeDefend(stack);
return BattleAction::makeMove(stack, currentDest);
}
currentDest = reachability.predecessors[currentDest];
}

View File

@ -17,7 +17,7 @@ class EnemyInfo;
class CStupidAI : public CBattleGameInterface
{
int side;
BattleSide side;
std::shared_ptr<CBattleCallback> cb;
std::shared_ptr<Environment> env;
@ -47,7 +47,7 @@ public:
void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) override;
void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) override;//called when a specific effect is set to stacks
//void battleTriggerEffect(const BattleTriggerEffect & bte) override;
void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right
void battleStart(const BattleID & battleID, const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, BattleSide side, bool replayAllowed) override; //called by engine when battle starts; side=0 - left, side=1 - right
void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override; //called when catapult makes an attack
private:

View File

@ -1,152 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|Win32">
<Configuration>Debug</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="RD|Win32">
<Configuration>RD</Configuration>
<Platform>Win32</Platform>
</ProjectConfiguration>
<ProjectConfiguration Include="RD|x64">
<Configuration>RD</Configuration>
<Platform>x64</Platform>
</ProjectConfiguration>
</ItemGroup>
<PropertyGroup Label="Globals">
<ProjectGuid>{15DABC90-234A-4B6B-9EEB-777C4768B82B}</ProjectGuid>
<RootNamespace>StupidAI</RootNamespace>
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>MultiByte</CharacterSet>
<PlatformToolset>v140_xp</PlatformToolset>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>true</UseDebugLibraries>
<CharacterSet>MultiByte</CharacterSet>
<PlatformToolset>v140_xp</PlatformToolset>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='RD|Win32'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>MultiByte</CharacterSet>
<PlatformToolset>v142</PlatformToolset>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='RD|x64'" Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<CharacterSet>MultiByte</CharacterSet>
<PlatformToolset>v140_xp</PlatformToolset>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="..\..\VCMI_global_debug.props" />
<Import Project="..\..\VCMI_global.props" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="..\..\VCMI_global_debug.props" />
<Import Project="..\..\VCMI_global.props" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='RD|Win32'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="..\..\VCMI_global_release.props" />
<Import Project="..\..\VCMI_global.props" />
</ImportGroup>
<ImportGroup Condition="'$(Configuration)|$(Platform)'=='RD|x64'" Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
<Import Project="..\..\VCMI_global_release.props" />
<Import Project="..\..\VCMI_global.props" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<OutDir>$(VCMI_Out)\AI\</OutDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<OutDir>$(VCMI_Out)\AI\</OutDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='RD|Win32'">
<OutDir>$(VCMI_Out)/AI</OutDir>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='RD|x64'">
<OutDir>$(VCMI_Out)\AI\</OutDir>
</PropertyGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>StdInc.h</PrecompiledHeaderFile>
<AdditionalOptions>/Zm150 %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
<AdditionalDependencies>VCMI_lib.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalOptions>-Zm150 %(AdditionalOptions)</AdditionalOptions>
<AdditionalLibraryDirectories>..\..\..\libs;..\..;..</AdditionalLibraryDirectories>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>StdInc.h</PrecompiledHeaderFile>
<AdditionalOptions>/Zm150 %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
<AdditionalDependencies>VCMI_lib.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='RD|Win32'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>StdInc.h</PrecompiledHeaderFile>
<MultiProcessorCompilation>true</MultiProcessorCompilation>
</ClCompile>
<Link>
<AdditionalDependencies>VCMI_lib.lib;%(AdditionalDependencies)</AdditionalDependencies>
<AdditionalLibraryDirectories>$(VCMI_Out)</AdditionalLibraryDirectories>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='RD|x64'">
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>StdInc.h</PrecompiledHeaderFile>
<AdditionalOptions>/Zm150 %(AdditionalOptions)</AdditionalOptions>
</ClCompile>
<Link>
<AdditionalDependencies>VCMI_lib.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClCompile Include="main.cpp" />
<ClCompile Include="StdInc.cpp">
<PreprocessorDefinitions Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">%(PreprocessorDefinitions)</PreprocessorDefinitions>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">Create</PrecompiledHeader>
<PrecompiledHeaderFile Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">StdInc.h</PrecompiledHeaderFile>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='RD|Win32'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">Create</PrecompiledHeader>
<PrecompiledHeader Condition="'$(Configuration)|$(Platform)'=='RD|x64'">Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="StupidAI.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="StdInc.h" />
<ClInclude Include="StupidAI.h" />
<ClInclude Include="..\..\Global.h" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
</ImportGroup>
</Project>

View File

@ -15,8 +15,6 @@
#include "../../lib/UnlockGuard.h"
#include "../../lib/CConfigHandler.h"
#include "../../lib/CHeroHandler.h"
#include "../../lib/mapObjects/CBank.h"
#include "../../lib/mapObjects/CGTownInstance.h"
#include "../../lib/mapObjects/CQuest.h"
#include "../../lib/mapping/CMapDefines.h"
@ -188,7 +186,7 @@ bool canBeEmbarkmentPoint(const TerrainTile * t, bool fromWater)
{
// TODO: Such information should be provided by pathfinder
// Tile must be free or with unoccupied boat
if(!t->blocked)
if(!t->blocked())
{
return true;
}
@ -249,8 +247,8 @@ bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2)
bool compareArtifacts(const CArtifactInstance * a1, const CArtifactInstance * a2)
{
auto art1 = a1->artType;
auto art2 = a2->artType;
auto art1 = a1->getType();
auto art2 = a2->getType();
if(art1->getPrice() == art2->getPrice())
return art1->valOfBonuses(BonusType::PRIMARY_SKILL) > art2->valOfBonuses(BonusType::PRIMARY_SKILL);

View File

@ -10,9 +10,7 @@
#pragma once
#include "../../lib/VCMI_Lib.h"
#include "../../lib/CBuildingHandler.h"
#include "../../lib/CCreatureHandler.h"
#include "../../lib/CTownHandler.h"
#include "../../lib/spells/CSpellHandler.h"
#include "../../lib/CStopWatch.h"
#include "../../lib/mapObjects/CGHeroInstance.h"
@ -27,11 +25,9 @@ using crstring = const std::string &;
using dwellingContent = std::pair<ui32, std::vector<CreatureID>>;
const int ACTUAL_RESOURCE_COUNT = 7;
const int ALLOWED_ROAMING_HEROES = 8;
//implementation-dependent
extern const double SAFE_ATTACK_CONSTANT;
extern const int GOLD_RESERVE;
extern thread_local CCallback * cb;
extern thread_local VCAI * ai;
@ -68,14 +64,6 @@ public:
const CGHeroInstance * get(bool doWeExpectNull = false) const;
bool validAndSet() const;
template<typename Handler> void serialize(Handler & h)
{
h & this->h;
h & hid;
h & name;
}
};
enum BattleState
@ -100,12 +88,6 @@ struct ObjectIdRef
ObjectIdRef(const CGObjectInstance * obj);
bool operator<(const ObjectIdRef & rhs) const;
template<typename Handler> void serialize(Handler & h)
{
h & id;
}
};
struct TimeCheck

View File

@ -36,7 +36,7 @@ std::vector<SlotInfo> ArmyManager::getSortedSlots(const CCreatureSet * target, c
{
for(auto & i : armyPtr->Slots())
{
auto cre = dynamic_cast<const CCreature*>(i.second->type);
auto cre = dynamic_cast<const CCreature*>(i.second->getType());
auto & slotInfp = creToPower[cre];
slotInfp.creature = cre;

View File

@ -14,8 +14,6 @@
#include "../../lib/GameConstants.h"
#include "../../lib/VCMI_Lib.h"
#include "../../lib/CTownHandler.h"
#include "../../lib/CBuildingHandler.h"
#include "VCAI.h"
struct SlotInfo

View File

@ -13,6 +13,7 @@
#include "../../CCallback.h"
#include "../../lib/mapObjects/MapObjects.h"
#include "../../lib/entities/building/CBuilding.h"
bool BuildingManager::tryBuildThisStructure(const CGTownInstance * t, BuildingID building, unsigned int maxDays)
{
@ -22,13 +23,13 @@ bool BuildingManager::tryBuildThisStructure(const CGTownInstance * t, BuildingID
return false;
}
if (!vstd::contains(t->town->buildings, building))
if (!vstd::contains(t->getTown()->buildings, building))
return false; // no such building in town
if (t->hasBuilt(building)) //Already built? Shouldn't happen in general
return true;
const CBuilding * buildPtr = t->town->buildings.at(building);
const CBuilding * buildPtr = t->getTown()->buildings.at(building);
auto toBuild = buildPtr->requirements.getFulfillmentCandidates([&](const BuildingID & buildID)
{
@ -50,7 +51,7 @@ bool BuildingManager::tryBuildThisStructure(const CGTownInstance * t, BuildingID
for (const auto & buildID : toBuild)
{
const CBuilding * b = t->town->buildings.at(buildID);
const CBuilding * b = t->getTown()->buildings.at(buildID);
EBuildingState canBuild = cb->canBuildStructure(t, buildID);
if (canBuild == EBuildingState::ALLOWED)
@ -142,9 +143,9 @@ static const std::vector<BuildingID> basicGoldSource = { BuildingID::TOWN_HALL,
static const std::vector<BuildingID> defence = { BuildingID::FORT, BuildingID::CITADEL, BuildingID::CASTLE };
static const std::vector<BuildingID> capitolAndRequirements = { BuildingID::FORT, BuildingID::CITADEL, BuildingID::CASTLE, BuildingID::CAPITOL };
static const std::vector<BuildingID> unitsSource = { BuildingID::DWELL_LVL_1, BuildingID::DWELL_LVL_2, BuildingID::DWELL_LVL_3,
BuildingID::DWELL_LVL_4, BuildingID::DWELL_LVL_5, BuildingID::DWELL_LVL_6, BuildingID::DWELL_LVL_7 };
BuildingID::DWELL_LVL_4, BuildingID::DWELL_LVL_5, BuildingID::DWELL_LVL_6, BuildingID::DWELL_LVL_7, BuildingID::DWELL_LVL_8 };
static const std::vector<BuildingID> unitsUpgrade = { BuildingID::DWELL_LVL_1_UP, BuildingID::DWELL_LVL_2_UP, BuildingID::DWELL_LVL_3_UP,
BuildingID::DWELL_LVL_4_UP, BuildingID::DWELL_LVL_5_UP, BuildingID::DWELL_LVL_6_UP, BuildingID::DWELL_LVL_7_UP };
BuildingID::DWELL_LVL_4_UP, BuildingID::DWELL_LVL_5_UP, BuildingID::DWELL_LVL_6_UP, BuildingID::DWELL_LVL_7_UP, BuildingID::DWELL_LVL_8_UP };
static const std::vector<BuildingID> unitGrowth = { BuildingID::HORDE_1, BuildingID::HORDE_1_UPGR, BuildingID::HORDE_2, BuildingID::HORDE_2_UPGR };
static const std::vector<BuildingID> _spells = { BuildingID::MAGES_GUILD_1, BuildingID::MAGES_GUILD_2, BuildingID::MAGES_GUILD_3,
BuildingID::MAGES_GUILD_4, BuildingID::MAGES_GUILD_5 };
@ -195,7 +196,7 @@ bool BuildingManager::getBuildingOptions(const CGTownInstance * t)
return true;
//workaround for mantis #2696 - build capitol with separate algorithm if it is available
if(vstd::contains(t->builtBuildings, BuildingID::CITY_HALL) && getMaxPossibleGoldBuilding(t) == BuildingID::CAPITOL)
if(t->hasBuilt(BuildingID::CITY_HALL) && getMaxPossibleGoldBuilding(t) == BuildingID::CAPITOL)
{
if(tryBuildNextStructure(t, capitolAndRequirements))
return true;
@ -219,7 +220,7 @@ bool BuildingManager::getBuildingOptions(const CGTownInstance * t)
//at the end, try to get and build any extra buildings with nonstandard slots (for example HotA 3rd level dwelling)
std::vector<BuildingID> extraBuildings;
for (auto buildingInfo : t->town->buildings)
for (auto buildingInfo : t->getTown()->buildings)
{
if (buildingInfo.first > BuildingID::DWELL_UP2_FIRST)
extraBuildings.push_back(buildingInfo.first);

View File

@ -14,8 +14,6 @@
#include "../../lib/GameConstants.h"
#include "../../lib/VCMI_Lib.h"
#include "../../lib/CTownHandler.h"
#include "../../lib/CBuildingHandler.h"
#include "VCAI.h"
struct DLL_EXPORT PotentialBuilding

View File

@ -219,12 +219,6 @@ float TacticalAdvantageEngine::getTacticalAdvantage(const CArmedInstance * we, c
enemyFlyers->setValue(enemyStructure.flyers);
enemySpeed->setValue(enemyStructure.maxSpeed);
bool bank = dynamic_cast<const CBank *>(enemy);
if(bank)
bankPresent->setValue(1);
else
bankPresent->setValue(0);
const CGTownInstance * fort = dynamic_cast<const CGTownInstance *>(enemy);
if(fort)
castleWalls->setValue(fort->fortLevel());

View File

@ -16,7 +16,6 @@
#include "../../lib/mapObjectConstructors/AObjectTypeHandler.h"
#include "../../lib/mapObjectConstructors/CObjectClassesHandler.h"
#include "../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
#include "../../lib/mapObjects/CBank.h"
#include "../../lib/mapObjects/CGCreature.h"
#include "../../lib/mapObjects/CGDwelling.h"
#include "../../lib/gameState/InfoAboutArmy.h"
@ -62,25 +61,6 @@ Goals::TSubgoal FuzzyHelper::chooseSolution(Goals::TGoalVec vec)
return result;
}
ui64 FuzzyHelper::estimateBankDanger(const CBank * bank)
{
//this one is not fuzzy anymore, just calculate weighted average
auto objectInfo = bank->getObjectHandler()->getObjectInfo(bank->appearance);
CBankInfo * bankInfo = dynamic_cast<CBankInfo *>(objectInfo.get());
ui64 totalStrength = 0;
ui8 totalChance = 0;
for(auto config : bankInfo->getPossibleGuards(bank->cb))
{
totalStrength += config.second.totalStrength * config.first;
totalChance += config.first;
}
return totalStrength / std::max<ui8>(totalChance, 1); //avoid division by zero
}
float FuzzyHelper::evaluate(Goals::VisitTile & g)
{
if(g.parent)
@ -301,32 +281,13 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj, const VCAI * ai)
cb->getTownInfo(obj, iat);
return iat.army.getStrength();
}
case Obj::MONSTER:
{
//TODO!!!!!!!!
const CGCreature * cre = dynamic_cast<const CGCreature *>(obj);
return cre->getArmyStrength();
}
case Obj::CREATURE_GENERATOR1:
case Obj::CREATURE_GENERATOR4:
{
const CGDwelling * d = dynamic_cast<const CGDwelling *>(obj);
return d->getArmyStrength();
}
case Obj::MINE:
case Obj::ABANDONED_MINE:
default:
{
const CArmedInstance * a = dynamic_cast<const CArmedInstance *>(obj);
return a->getArmyStrength();
if (a)
return a->getArmyStrength();
else
return 0;
}
case Obj::CRYPT: //crypt
case Obj::CREATURE_BANK: //crebank
case Obj::DRAGON_UTOPIA:
case Obj::SHIPWRECK: //shipwreck
case Obj::DERELICT_SHIP: //derelict ship
case Obj::PYRAMID:
return estimateBankDanger(dynamic_cast<const CBank *>(obj));
default:
return 0;
}
}

View File

@ -10,12 +10,6 @@
#pragma once
#include "FuzzyEngines.h"
VCMI_LIB_NAMESPACE_BEGIN
class CBank;
VCMI_LIB_NAMESPACE_END
class DLL_EXPORT FuzzyHelper
{
public:
@ -42,8 +36,6 @@ public:
float evaluate(Goals::AbstractGoal & g);
void setPriority(Goals::TSubgoal & g);
ui64 estimateBankDanger(const CBank * bank); //TODO: move to another class?
Goals::TSubgoal chooseSolution(Goals::TGoalVec vec);
//std::shared_ptr<AbstractGoal> chooseSolution (std::vector<std::shared_ptr<AbstractGoal>> & vec);

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