1
0
mirror of https://github.com/vcmi/vcmi.git synced 2025-08-06 22:23:15 +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
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** **To Reproduce**
Steps to reproduce the behavior: Steps to reproduce the behavior:
1. Go to '...' 1. Go to '...'
2. Click on '....' 2. Click on '....'
3. Scroll down to '....' 3. Scroll down to '....'
@ -24,7 +25,7 @@ Steps to reproduce the behavior:
A clear and concise description of what you expected to happen. A clear and concise description of what you expected to happen.
**Actual behavior** **Actual behavior**
A clear description what is currently happening A clear description what is currently happening
**Did it work earlier?** **Did it work earlier?**
If this something which worked well some time ago, please let us know about version where it works or at date when it worked. If this something which worked well some time ago, please let us know about version where it works or at date when it worked.
@ -33,8 +34,9 @@ If this something which worked well some time ago, please let us know about vers
If applicable, add screenshots to help explain your problem. If applicable, add screenshots to help explain your problem.
**Version** **Version**
- OS: [e.g. Windows, macOS Intel, macOS ARM, Android, Linux, iOS]
- Version: [VCMI version] - OS: [e.g. Windows, macOS Intel, macOS ARM, Android, Linux, iOS]
- Version: [VCMI version]
**Additional context** **Additional context**
Add any other context about the problem here. Add any other context about the problem here.

View File

@ -3,7 +3,6 @@ name: VCMI
on: on:
push: push:
branches: branches:
- features/*
- beta - beta
- master - master
- develop - develop
@ -22,84 +21,112 @@ jobs:
- platform: linux-qt6 - platform: linux-qt6
os: ubuntu-24.04 os: ubuntu-24.04
test: 0 test: 0
before_install: linux_qt6.sh
preset: linux-clang-test preset: linux-clang-test
- platform: linux - platform: linux
os: ubuntu-24.04 os: ubuntu-24.04
test: 1 test: 1
before_install: linux_qt5.sh
preset: linux-gcc-test preset: linux-gcc-test
- platform: linux - platform: linux
os: ubuntu-20.04 os: ubuntu-20.04
test: 0 test: 0
before_install: linux_qt5.sh
preset: linux-gcc-debug preset: linux-gcc-debug
- platform: mac-intel - platform: mac-intel
os: macos-13 os: macos-13
test: 0 test: 0
pack: 1 pack: 1
upload: 1
pack_type: Release pack_type: Release
extension: dmg extension: dmg
before_install: macos.sh
preset: macos-conan-ninja-release preset: macos-conan-ninja-release
conan_profile: macos-intel conan_profile: macos-intel
conan_prebuilts: dependencies-mac-intel
conan_options: --options with_apple_system_libs=True conan_options: --options with_apple_system_libs=True
artifact_platform: intel artifact_platform: intel
- platform: mac-arm - platform: mac-arm
os: macos-13 os: macos-13
test: 0 test: 0
pack: 1 pack: 1
upload: 1
pack_type: Release pack_type: Release
extension: dmg extension: dmg
before_install: macos.sh
preset: macos-arm-conan-ninja-release preset: macos-arm-conan-ninja-release
conan_profile: macos-arm conan_profile: macos-arm
conan_prebuilts: dependencies-mac-arm
conan_options: --options with_apple_system_libs=True conan_options: --options with_apple_system_libs=True
artifact_platform: arm artifact_platform: arm
- platform: ios - platform: ios
os: macos-13 os: macos-13
test: 0 test: 0
pack: 1 pack: 1
upload: 1
pack_type: Release pack_type: Release
extension: ipa extension: ipa
before_install: macos.sh
preset: ios-release-conan-ccache preset: ios-release-conan-ccache
conan_profile: ios-arm64 conan_profile: ios-arm64
conan_prebuilts: dependencies-ios
conan_options: --options with_apple_system_libs=True 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 os: windows-latest
test: 0 test: 0
pack: 1 pack: 1
pack_type: RelWithDebInfo pack_type: RelWithDebInfo
extension: exe extension: exe
preset: windows-msvc-release before_install: msvc.sh
- platform: mingw preset: windows-msvc-release-x86
os: ubuntu-22.04 - platform: mingw_x86_64
os: ubuntu-24.04
test: 0 test: 0
pack: 1 pack: 1
pack_type: Release pack_type: Release
extension: exe extension: exe
cpack_args: -D CPACK_NSIS_EXECUTABLE=`which makensis`
cmake_args: -G Ninja cmake_args: -G Ninja
before_install: mingw.sh
preset: windows-mingw-conan-linux preset: windows-mingw-conan-linux
conan_profile: mingw64-linux.jinja conan_profile: mingw64-linux.jinja
- platform: mingw-32 conan_prebuilts: dependencies-mingw-x86-64
os: ubuntu-22.04 - platform: mingw_x86
os: ubuntu-24.04
test: 0 test: 0
pack: 1 pack: 1
pack_type: Release pack_type: Release
extension: exe extension: exe
cpack_args: -D CPACK_NSIS_EXECUTABLE=`which makensis`
cmake_args: -G Ninja cmake_args: -G Ninja
before_install: mingw.sh
preset: windows-mingw-conan-linux preset: windows-mingw-conan-linux
conan_profile: mingw32-linux.jinja conan_profile: mingw32-linux.jinja
conan_prebuilts: dependencies-mingw-x86
- platform: android-32 - platform: android-32
os: macos-14 os: ubuntu-24.04
upload: 1
extension: apk extension: apk
preset: android-conan-ninja-release preset: android-conan-ninja-release
conan_profile: android-32 before_install: android.sh
conan_options: --conf tools.android:ndk_path=$ANDROID_NDK_ROOT conan_profile: android-32-ndk
conan_prebuilts: dependencies-android-armeabi-v7a
artifact_platform: armeabi-v7a artifact_platform: armeabi-v7a
- platform: android-64 - platform: android-64
os: macos-14 os: ubuntu-24.04
upload: 1
extension: apk extension: apk
preset: android-conan-ninja-release preset: android-conan-ninja-release
conan_profile: android-64 before_install: android.sh
conan_options: --conf tools.android:ndk_path=$ANDROID_NDK_ROOT conan_profile: android-64-ndk
conan_prebuilts: dependencies-android-arm64-v8a
artifact_platform: arm64-v8a artifact_platform: arm64-v8a
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
defaults: defaults:
@ -107,15 +134,25 @@ jobs:
shell: bash shell: bash
steps: steps:
- uses: actions/checkout@v4 - name: Checkout repository
uses: actions/checkout@v4
with: with:
submodules: recursive submodules: recursive
- name: Dependencies - name: Prepare CI
run: source '${{github.workspace}}/CI/${{matrix.platform}}/before_install.sh' if: "${{ matrix.before_install != '' }}"
run: source '${{github.workspace}}/CI/before_install/${{matrix.before_install}}'
env: env:
VCMI_BUILD_PLATFORM: x64 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 # 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 # fall back to ccache of the vcmi/vcmi repo if no PR-specific ccache is found
- name: ccache for PRs - name: ccache for PRs
@ -146,20 +183,24 @@ jobs:
HEROES_3_DATA_PASSWORD: ${{ secrets.HEROES_3_DATA_PASSWORD }} HEROES_3_DATA_PASSWORD: ${{ secrets.HEROES_3_DATA_PASSWORD }}
if: ${{ env.HEROES_3_DATA_PASSWORD != '' && matrix.test == 1 }} if: ${{ env.HEROES_3_DATA_PASSWORD != '' && matrix.test == 1 }}
run: | 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 7za x h3_assets.zip -p$HEROES_3_DATA_PASSWORD
mkdir -p ~/.local/share/vcmi/ mkdir -p ~/.local/share/vcmi/
mv h3_assets/* ~/.local/share/vcmi/ mv h3_assets/* ~/.local/share/vcmi/
- uses: actions/setup-python@v5 - name: Install Conan
if: "${{ matrix.conan_profile != '' }}" if: "${{ matrix.conan_profile != '' }}"
with: run: pipx install 'conan<2.0'
python-version: '3.10'
- name: Conan setup - name: Install Conan profile
if: "${{ matrix.conan_profile != '' }}" if: "${{ matrix.conan_profile != '' }}"
run: | run: |
pip3 install 'conan<2.0'
conan profile new default --detect conan profile new default --detect
conan install . \ conan install . \
--install-folder=conan-generated \ --install-folder=conan-generated \
@ -171,7 +212,13 @@ jobs:
env: env:
GENERATE_ONLY_BUILT_CONFIG: 1 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') }} if: ${{ startsWith(matrix.platform, 'android') }}
with: with:
distribution: 'temurin' distribution: 'temurin'
@ -202,11 +249,11 @@ jobs:
elif [[ (${{matrix.preset}} == android-conan-ninja-release) && (${{github.ref}} != 'refs/heads/master') ]] elif [[ (${{matrix.preset}} == android-conan-ninja-release) && (${{github.ref}} != 'refs/heads/master') ]]
then then
cmake -DENABLE_CCACHE:BOOL=ON -DANDROID_GRADLE_PROPERTIES="applicationIdSuffix=.daily;signingConfig=dailySigning;applicationLabel=VCMI daily" --preset ${{ matrix.preset }} 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 then
cmake -DENABLE_CCACHE:BOOL=ON --preset ${{ matrix.preset }}
else
cmake --preset ${{ matrix.preset }} cmake --preset ${{ matrix.preset }}
else
cmake -DENABLE_CCACHE:BOOL=ON --preset ${{ matrix.preset }}
fi fi
- name: Build - name: Build
@ -236,10 +283,13 @@ jobs:
if: ${{ matrix.pack == 1 }} if: ${{ matrix.pack == 1 }}
run: | run: |
cd '${{github.workspace}}/out/build/${{matrix.preset}}' 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 # Workaround for CPack bug on macOS 13
test -f '${{github.workspace}}/CI/${{matrix.platform}}/post_pack.sh' \ counter=0
&& '${{github.workspace}}/CI/${{matrix.platform}}/post_pack.sh' '${{github.workspace}}' "$(ls '${{ env.VCMI_PACKAGE_FILE_NAME }}'.*)" until cpack -C ${{matrix.pack_type}} || ((counter > 20)); do
sleep 3
((counter++))
done
rm -rf _CPack_Packages rm -rf _CPack_Packages
- name: Artifacts - name: Artifacts
@ -247,6 +297,7 @@ jobs:
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }}
compression-level: 0
path: | path: |
${{github.workspace}}/out/build/${{matrix.preset}}/${{ env.VCMI_PACKAGE_FILE_NAME }}.${{ matrix.extension }} ${{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_APK_PATH=$ANDROID_APK_PATH" >> $GITHUB_ENV
echo "ANDROID_AAB_PATH=$ANDROID_AAB_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') }} if: ${{ startsWith(matrix.platform, 'android') }}
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }}
compression-level: 0
path: | path: |
${{ env.ANDROID_APK_PATH }} ${{ env.ANDROID_APK_PATH }}
- name: Android aab artifacts - name: Upload Android aab artifacts
if: ${{ startsWith(matrix.platform, 'android') }} if: ${{ startsWith(matrix.platform, 'android') && github.ref == 'refs/heads/master' }}
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} - aab name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} - aab
compression-level: 0
path: | path: |
${{ env.ANDROID_AAB_PATH }} ${{ env.ANDROID_AAB_PATH }}
- name: Symbols - name: Upload debug symbols
if: ${{ matrix.platform == 'msvc' }} if: ${{ startsWith(matrix.platform, 'msvc') }}
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4
with: with:
name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} - symbols name: ${{ env.VCMI_PACKAGE_FILE_NAME }} - ${{ matrix.platform }} - symbols
compression-level: 9
path: | path: |
${{github.workspace}}/**/*.pdb ${{github.workspace}}/**/*.pdb
- name: Upload build - 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 continue-on-error: true
run: | run: |
if [ -z '${{ env.ANDROID_APK_PATH }}' ] ; then if [ -z '${{ env.ANDROID_APK_PATH }}' ] ; then
@ -337,19 +391,20 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: actions/setup-python@v5
if: "${{ matrix.conan_profile != '' }}"
with:
python-version: '3.10'
- name: Ensure LF line endings - name: Ensure LF line endings
run: | run: |
find . -path ./.git -prune -o -path ./AI/FuzzyLite -prune -o -path ./test/googletest \ find . -path ./.git -prune -o -path ./AI/FuzzyLite -prune -o -path ./test/googletest \
-o -path ./osx -prune -o -type f \ -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'; } { ! xargs -0 grep -l -z -P '\r\n'; }
- name: Validate JSON - name: Validate JSON
run: | run: |
sudo apt install python3-jstyleson 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 *.ipch
VCMI_VS11.opensdf VCMI_VS11.opensdf
.DS_Store .DS_Store
.directory
CMakeUserPresets.json CMakeUserPresets.json
compile_commands.json compile_commands.json
fuzzylite.pc fuzzylite.pc

2
.gitmodules vendored
View File

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

View File

@ -12,6 +12,10 @@
#include "../../lib/CStack.h" // TODO: remove #include "../../lib/CStack.h" // TODO: remove
// Eventually only IBattleInfoCallback and battle::Unit should be used, // Eventually only IBattleInfoCallback and battle::Unit should be used,
// CUnitState should be private and CStack should be removed completely // 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) 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(); damageCache[attacker->unitId()][defender->unitId()] = static_cast<float>(damage) / attacker->getCount();
} }
void DamageCache::buildObstacleDamageCache(std::shared_ptr<HypotheticBattle> hb, BattleSide side)
void DamageCache::buildDamageCache(std::shared_ptr<HypotheticBattle> hb, int 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 auto stacks = hb->battleGetUnitsIf([=](const battle::Unit * u) -> bool
{ {
return u->isValidTarget(); 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(); 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) int64_t DamageCache::getOriginalDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr<CBattleInfoCallback> hb)
{ {
if(parent) 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) AttackPossibility::AttackPossibility(BattleHex from, BattleHex dest, const BattleAttackInfo & attack)
: from(from), dest(dest), attack(attack) : from(from), dest(dest), attack(attack)
{ {
this->attack.attackerPos = from;
this->attack.defenderPos = dest;
} }
float AttackPossibility::damageDiff() const float AttackPossibility::damageDiff() const
@ -199,6 +277,8 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(
if(attackInfo.shooting) if(attackInfo.shooting)
return 0; return 0;
std::set<uint32_t> checkedUnits;
auto attacker = attackInfo.attacker; auto attacker = attackInfo.attacker;
auto hexes = attacker->getSurroundingHexes(hex); auto hexes = attacker->getSurroundingHexes(hex);
for(BattleHex tile : hexes) for(BattleHex tile : hexes)
@ -206,9 +286,13 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(
auto st = state->battleGetUnitByPos(tile, true); auto st = state->battleGetUnitByPos(tile, true);
if(!st || !state->battleMatchOwner(st, attacker)) if(!st || !state->battleMatchOwner(st, attacker))
continue; continue;
if(vstd::contains(checkedUnits, st->unitId()))
continue;
if(!state->battleCanShoot(st)) if(!state->battleCanShoot(st))
continue; continue;
checkedUnits.insert(st->unitId());
// FIXME: provide distance info for Jousting bonus // FIXME: provide distance info for Jousting bonus
BattleAttackInfo rangeAttackInfo(st, attacker, 0, true); BattleAttackInfo rangeAttackInfo(st, attacker, 0, true);
rangeAttackInfo.defenderPos = hex; rangeAttackInfo.defenderPos = hex;
@ -218,9 +302,10 @@ int64_t AttackPossibility::evaluateBlockedShootersDmg(
auto rangeDmg = state->battleEstimateDamage(rangeAttackInfo); auto rangeDmg = state->battleEstimateDamage(rangeAttackInfo);
auto meleeDmg = state->battleEstimateDamage(meleeAttackInfo); auto meleeDmg = state->battleEstimateDamage(meleeAttackInfo);
auto cachedDmg = damageCache.getOriginalDamage(st, attacker, state);
int64_t gain = averageDmg(rangeDmg.damage) - averageDmg(meleeDmg.damage) + 1; 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; return res;
@ -243,7 +328,7 @@ AttackPossibility AttackPossibility::evaluate(
std::vector<BattleHex> defenderHex; std::vector<BattleHex> defenderHex;
if(attackInfo.shooting) if(attackInfo.shooting)
defenderHex = defender->getHexes(); defenderHex.push_back(defender->getPosition());
else else
defenderHex = CStack::meleeAttackHexes(attacker, defender, hex); defenderHex = CStack::meleeAttackHexes(attacker, defender, hex);
@ -261,63 +346,114 @@ AttackPossibility AttackPossibility::evaluate(
if (!attackInfo.shooting) if (!attackInfo.shooting)
ap.attackerState->setPosition(hex); 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) if (attackInfo.shooting)
units = state->getAttackedBattleUnits(attacker, defHex, true, BattleHex::INVALID); defenderUnits = state->getAttackedBattleUnits(attacker, defender, defHex, true, hex, defender->getPosition());
else 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; retaliatedUnits.push_back(attacker);
break; }
auto obstacleDamage = damageCache.getObstacleDamage(hex, attacker);
if(obstacleDamage > 0)
{
ap.attackerDamageReduce += calculateDamageReduce(nullptr, attacker, obstacleDamage, damageCache, state);
ap.attackerState->damage(obstacleDamage);
} }
} }
if(addDefender) // ensure the defender is also affected
units.push_back(defender); if(!vstd::contains_if(defenderUnits, [defender](const battle::Unit * u) -> bool { return u->unitId() == defender->unitId(); }))
for(auto u : units)
{ {
if(!ap.attackerState->alive()) defenderUnits.push_back(defender);
break; }
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(); 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 damageDealt;
int64_t damageReceived;
float defenderDamageReduce; float defenderDamageReduce;
float attackerDamageReduce; float attackerDamageReduce;
DamageEstimation retaliation; DamageEstimation retaliation;
auto attackDmg = state->battleEstimateDamage(ap.attack, &retaliation); auto attackDmg = state->battleEstimateDamage(ap.attack, &retaliation);
vstd::amin(attackDmg.damage.min, defenderState->getAvailableHealth());
vstd::amin(attackDmg.damage.max, defenderState->getAvailableHealth());
vstd::amin(retaliation.damage.min, ap.attackerState->getAvailableHealth());
vstd::amin(retaliation.damage.max, ap.attackerState->getAvailableHealth());
damageDealt = averageDmg(attackDmg.damage); 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); ap.attackerState->afterAttack(attackInfo.shooting, false);
//FIXME: use ranged retaliation //FIXME: use ranged retaliation
damageReceived = 0;
attackerDamageReduce = 0; attackerDamageReduce = 0;
if (!attackInfo.shooting && defenderState->ableToRetaliate() && !counterAttacksBlocked) if (!attackInfo.shooting && u->unitId() == defender->unitId() && defenderState->ableToRetaliate() && !counterAttacksBlocked)
{ {
damageReceived = averageDmg(retaliation.damage); for(auto retaliated : retaliatedUnits)
attackerDamageReduce = calculateDamageReduce(defender, attacker, damageReceived, damageCache, state); {
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); defenderState->afterAttack(attackInfo.shooting, true);
} }
@ -331,21 +467,30 @@ AttackPossibility AttackPossibility::evaluate(
if(attackerSide == u->unitSide()) if(attackerSide == u->unitSide())
ap.collateralDamageReduce += defenderDamageReduce; ap.collateralDamageReduce += defenderDamageReduce;
if(u->unitId() == defender->unitId() || if(u->unitId() == defender->unitId()
(!attackInfo.shooting && CStack::isMeleeAttackPossible(u, attacker, hex))) || (!attackInfo.shooting && CStack::isMeleeAttackPossible(u, attacker, hex)))
{ {
//FIXME: handle RANGED_RETALIATION ? //FIXME: handle RANGED_RETALIATION ?
ap.attackerDamageReduce += attackerDamageReduce; ap.attackerDamageReduce += attackerDamageReduce;
} }
ap.attackerState->damage(damageReceived);
defenderState->damage(damageDealt); defenderState->damage(damageDealt);
if (!ap.attackerState->alive() || !defenderState->alive()) if(u->unitId() == defender->unitId())
break; {
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()) if(!bestAp.dest.isValid() || ap.attackValue() > bestAp.attackValue())
bestAp = ap; bestAp = ap;
} }

View File

@ -18,16 +18,20 @@ class DamageCache
{ {
private: private:
std::unordered_map<uint32_t, std::unordered_map<uint32_t, float>> damageCache; 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; DamageCache * parent;
void buildObstacleDamageCache(std::shared_ptr<HypotheticBattle> hb, BattleSide side);
public: public:
DamageCache() : parent(nullptr) {} DamageCache() : parent(nullptr) {}
DamageCache(DamageCache * parent) : parent(parent) {} DamageCache(DamageCache * parent) : parent(parent) {}
void cacheDamage(const battle::Unit * attacker, const battle::Unit * defender, std::shared_ptr<CBattleInfoCallback> hb); 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 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); 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> /// <summary>
@ -49,6 +53,7 @@ public:
float attackerDamageReduce = 0; //usually by counter-attack float attackerDamageReduce = 0; //usually by counter-attack
float collateralDamageReduce = 0; // friendly fire (usually by two-hex attacks) float collateralDamageReduce = 0; // friendly fire (usually by two-hex attacks)
int64_t shootersBlockedDmg = 0; int64_t shootersBlockedDmg = 0;
bool defenderDead = false;
AttackPossibility(BattleHex from, BattleHex dest, const BattleAttackInfo & attack_); 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/BattleAction.h"
#include "../../lib/battle/BattleStateInfoForRetreat.h" #include "../../lib/battle/BattleStateInfoForRetreat.h"
#include "../../lib/battle/CObstacleInstance.h" #include "../../lib/battle/CObstacleInstance.h"
#include "../../lib/StartInfo.h"
#include "../../lib/CStack.h" // TODO: remove #include "../../lib/CStack.h" // TODO: remove
// Eventually only IBattleInfoCallback and battle::Unit should be used, // Eventually only IBattleInfoCallback and battle::Unit should be used,
// CUnitState should be private and CStack should be removed completely // CUnitState should be private and CStack should be removed completely
#include "../../lib/logging/VisualLogger.h"
#define LOGL(text) print(text) #define LOGL(text) print(text)
#define LOGFL(text, formattingEl) print(boost::str(boost::format(text) % formattingEl)) #define LOGFL(text, formattingEl) print(boost::str(boost::format(text) % formattingEl))
CBattleAI::CBattleAI() CBattleAI::CBattleAI()
: side(-1), : side(BattleSide::NONE),
wasWaitingForRealize(false), wasWaitingForRealize(false),
wasUnlockingGs(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) void CBattleAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB)
{ {
env = ENV; env = ENV;
@ -57,6 +70,8 @@ void CBattleAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::share
CB->waitTillRealize = false; CB->waitTillRealize = false;
CB->unlockGsWhenWaiting = false; CB->unlockGsWhenWaiting = false;
movesSkippedByDefense = 0; movesSkippedByDefense = 0;
logHexNumbers();
} }
void CBattleAI::initBattleInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CBattleCallback> CB, AutocombatPreferences autocombatPreferences) 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())); 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 stacks = cb->battleGetAllStacks();
auto our = 0; 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; 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 ) void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack )
{ {
LOG_TRACE_PARAMS(logAi, "stack: %s", stack->nodeName()); 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"); logAi->trace("Build evaluator and targets");
#endif #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); result = evaluator.selectStackAction(stack);
if(autobattlePreferences.enableSpellsUsage && !skipCastUntilNextBattle && evaluator.canCastSpell()) if(autobattlePreferences.enableSpellsUsage && evaluator.canCastSpell())
{ {
auto spelCasted = evaluator.attemptCastingSpell(stack); auto spelCasted = evaluator.attemptCastingSpell(stack);
if(spelCasted) if(spelCasted)
return; return;
skipCastUntilNextBattle = true;
} }
logAi->trace("Spellcast attempt completed in %lld", timeElapsed(start)); logAi->trace("Spellcast attempt completed in %lld", timeElapsed(start));
@ -176,7 +197,7 @@ void CBattleAI::activeStack(const BattleID & battleID, const CStack * stack )
movesSkippedByDefense = 0; movesSkippedByDefense = 0;
} }
logAi->trace("BattleAI decission made in %lld", timeElapsed(start)); logAi->trace("BattleAI decision made in %lld", timeElapsed(start));
cb->battleMakeUnitAction(battleID, result); cb->battleMakeUnitAction(battleID, result);
} }
@ -206,7 +227,7 @@ BattleAction CBattleAI::useCatapult(const BattleID & battleID, const CStack * st
{ {
auto wallState = cb->getBattle(battleID)->battleGetWallState(wallPart); 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); targetHex = cb->getBattle(battleID)->wallPartToBattleHex(wallPart);
break; break;
@ -229,12 +250,10 @@ BattleAction CBattleAI::useCatapult(const BattleID & battleID, const CStack * st
return attack; 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); LOG_TRACE(logAi);
side = Side; side = Side;
skipCastUntilNextBattle = false;
} }
void CBattleAI::print(const std::string &text) const void CBattleAI::print(const std::string &text) const

View File

@ -27,7 +27,7 @@ struct CurrentOffensivePotential
std::map<const CStack *, PotentialTargets> ourAttacks; std::map<const CStack *, PotentialTargets> ourAttacks;
std::map<const CStack *, PotentialTargets> enemyAttacks; std::map<const CStack *, PotentialTargets> enemyAttacks;
CurrentOffensivePotential(ui8 side) CurrentOffensivePotential(BattleSide side)
{ {
for(auto stack : cbc->battleGetStacks()) for(auto stack : cbc->battleGetStacks())
{ {
@ -50,11 +50,11 @@ struct CurrentOffensivePotential
return ourPotential - enemyPotential; 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 class CBattleAI : public CBattleGameInterface
{ {
int side; BattleSide side;
std::shared_ptr<CBattleCallback> cb; std::shared_ptr<CBattleCallback> cb;
std::shared_ptr<Environment> env; std::shared_ptr<Environment> env;
@ -62,7 +62,6 @@ class CBattleAI : public CBattleGameInterface
bool wasWaitingForRealize; bool wasWaitingForRealize;
bool wasUnlockingGs; bool wasUnlockingGs;
int movesSkippedByDefense; int movesSkippedByDefense;
bool skipCastUntilNextBattle;
public: public:
CBattleAI(); CBattleAI();
@ -80,7 +79,7 @@ public:
BattleAction useCatapult(const BattleID & battleID, const CStack *stack); BattleAction useCatapult(const BattleID & battleID, const CStack *stack);
BattleAction useHealingTent(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 actionFinished(const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero
//void actionStarted(const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero //void actionStarted(const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero
//void battleAttack(const BattleAttack *ba) override; //called when stack is performing attack //void battleAttack(const BattleAttack *ba) override; //called when stack is performing attack
@ -93,7 +92,7 @@ public:
//void battleSpellCast(const BattleSpellCast *sc) override; //void battleSpellCast(const BattleSpellCast *sc) override;
//void battleStacksEffectsSet(const SetStackEffect & sse) override;//called when a specific effect is set to stacks //void battleStacksEffectsSet(const SetStackEffect & sse) override;//called when a specific effect is set to stacks
//void battleTriggerEffect(const BattleTriggerEffect & bte) override; //void battleTriggerEffect(const BattleTriggerEffect & bte) override;
//void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) 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 //void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack
AutocombatPreferences autobattlePreferences = AutocombatPreferences(); 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/CStopWatch.h"
#include "../../lib/CThreadHelper.h" #include "../../lib/CThreadHelper.h"
#include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/mapObjects/CGTownInstance.h"
#include "../../lib/entities/building/TownFortifications.h"
#include "../../lib/spells/CSpellHandler.h" #include "../../lib/spells/CSpellHandler.h"
#include "../../lib/spells/ISpellMechanics.h" #include "../../lib/spells/ISpellMechanics.h"
#include "../../lib/battle/BattleStateInfoForRetreat.h" #include "../../lib/battle/BattleStateInfoForRetreat.h"
@ -49,6 +50,43 @@ SpellTypes spellType(const CSpell * spell)
return SpellTypes::OTHER; 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> BattleEvaluator::getBrokenWallMoatHexes() const
{ {
std::vector<BattleHex> result; std::vector<BattleHex> result;
@ -81,6 +119,14 @@ std::vector<BattleHex> BattleEvaluator::getBrokenWallMoatHexes() const
return result; 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) std::optional<PossibleSpellcast> BattleEvaluator::findBestCreatureSpell(const CStack *stack)
{ {
//TODO: faerie dragon type spell should be selected by server //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); auto moveTarget = scoreEvaluator.findMoveTowardsUnreachable(stack, *targets, damageCache, hb);
float score = EvaluationResult::INEFFECTIVE_SCORE; 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()) 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()); logAi->trace("Evaluating attack for %s", stack->getDescription());
#endif #endif
auto evaluationResult = scoreEvaluator.findBestTarget(stack, *targets, damageCache, hb); auto evaluationResult = scoreEvaluator.findBestTarget(stack, *targets, damageCache, hb, siegeDefense);
auto & bestAttack = evaluationResult.bestAttack; auto & bestAttack = evaluationResult.bestAttack;
cachedAttack = bestAttack; cachedAttack.ap = bestAttack;
cachedScore = evaluationResult.score; 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. //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()) if(bestSpellcast.has_value() && bestSpellcast->value > bestAttack.damageDiff())
@ -167,7 +223,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
score score
); );
if (moveTarget.scorePerTurn <= score) if (moveTarget.score <= score)
{ {
if(evaluationResult.wait) if(evaluationResult.wait)
{ {
@ -186,37 +242,59 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
{ {
return BattleAction::makeDefend(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; logAi->trace("Evaluating exchange at %d self-defense", stack->getPosition().hex);
return BattleAction::makeMeleeAttack(stack, bestAttack.attack.defender->getPosition(), bestAttack.from);
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. //ThreatMap threatsToUs(stack); // These lines may be useful but they are't used in the code.
if(moveTarget.scorePerTurn > score) if(moveTarget.score > score)
{ {
score = moveTarget.score; score = moveTarget.score;
cachedAttack = moveTarget.cachedAttack; cachedAttack.ap = moveTarget.cachedAttack;
cachedScore = score; cachedAttack.score = score;
cachedAttack.turn = moveTarget.turnsToRich;
if(stack->waited()) if(stack->waited())
{ {
logAi->debug( logAi->debug(
"Moving %s towards hex %s[%d], score: %2f/%2f", "Moving %s towards hex %s[%d], score: %2f",
stack->getDescription(), stack->getDescription(),
moveTarget.cachedAttack->attack.defender->getDescription(), moveTarget.cachedAttack->attack.defender->getDescription(),
moveTarget.cachedAttack->attack.defender->getPosition().hex, moveTarget.cachedAttack->attack.defender->getPosition().hex,
moveTarget.score, moveTarget.score);
moveTarget.scorePerTurn);
return goTowardsNearest(stack, moveTarget.positions); return goTowardsNearest(stack, moveTarget.positions, *targets);
} }
else else
{ {
cachedAttack.waited = true;
return BattleAction::makeWait(stack); return BattleAction::makeWait(stack);
} }
} }
@ -224,7 +302,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
if(score <= EvaluationResult::INEFFECTIVE_SCORE if(score <= EvaluationResult::INEFFECTIVE_SCORE
&& !stack->hasBonusOfType(BonusType::FLYING) && !stack->hasBonusOfType(BonusType::FLYING)
&& stack->unitSide() == BattleSide::ATTACKER && stack->unitSide() == BattleSide::ATTACKER
&& cb->getBattle(battleID)->battleGetSiegeLevel() >= CGTownInstance::CITADEL) && cb->getBattle(battleID)->battleGetFortifications().hasMoat)
{ {
auto brokenWallMoat = getBrokenWallMoatHexes(); auto brokenWallMoat = getBrokenWallMoatHexes();
@ -235,7 +313,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
if(stack->doubleWide() && vstd::contains(brokenWallMoat, stack->getPosition())) if(stack->doubleWide() && vstd::contains(brokenWallMoat, stack->getPosition()))
return BattleAction::makeMove(stack, stack->getPosition().cloneInDirection(BattleHex::RIGHT)); return BattleAction::makeMove(stack, stack->getPosition().cloneInDirection(BattleHex::RIGHT));
else 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(); 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 reachability = cb->getBattle(battleID)->getReachability(stack);
auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false); 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 if(!avHexes.size() || !hexes.size()) //we are blocked or dest is blocked
{ {
return BattleAction::makeDefend(stack); return BattleAction::makeDefend(stack);
@ -261,56 +383,45 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector
std::vector<BattleHex> targetHexes = hexes; std::vector<BattleHex> targetHexes = hexes;
for(int i = 0; i < 5; i++) vstd::erase_if(targetHexes, [](const BattleHex & hex) { return !hex.isValid(); });
{
std::sort(targetHexes.begin(), targetHexes.end(), [&](BattleHex h1, BattleHex h2) -> bool
{
return reachability.distances.at(h1) < reachability.distances.at(h2);
});
for(auto hex : targetHexes) std::sort(targetHexes.begin(), targetHexes.end(), [&](BattleHex h1, BattleHex h2) -> bool
{ {
if(vstd::contains(avHexes, hex)) return reachability.distances[h1] < reachability.distances[h2];
{ });
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);
}
BattleHex bestNeighbor = targetHexes.front(); 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); 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); scoreEvaluator.updateReachabilityMap(hb);
if(stack->hasBonusOfType(BonusType::FLYING)) if(stack->hasBonusOfType(BonusType::FLYING))
{ {
std::set<BattleHex> obstacleHexes; 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(); auto affectedHexes = spellObst.getAffectedTiles();
obstacleHexes.insert(affectedHexes.cbegin(), affectedHexes.cend()); 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 scoreEvaluator.checkPositionBlocksOurStacks(*hb, stack, hex) ? BLOCKED_STACK_PENALTY + distance : distance;
}); });
return BattleAction::makeMove(stack, *nearestAvailableHex); return moveOrAttack(stack, *nearestAvailableHex, targets);
} }
else else
{ {
@ -357,11 +468,16 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector
if(vstd::contains(avHexes, currentDest) if(vstd::contains(avHexes, currentDest)
&& !scoreEvaluator.checkPositionBlocksOurStacks(*hb, stack, currentDest)) && !scoreEvaluator.checkPositionBlocksOurStacks(*hb, stack, currentDest))
return BattleAction::makeMove(stack, currentDest); {
return moveOrAttack(stack, currentDest, targets);
}
currentDest = reachability.predecessors[currentDest]; 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() bool BattleEvaluator::canCastSpell()
@ -391,7 +507,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
vstd::erase_if(possibleSpells, [](const CSpell *s) 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()); 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); spells::BattleCast temp(cb->getBattle(battleID).get(), hero, spells::Mode::HERO, spell);
if(spell->getTargetType() == spells::AimType::LOCATION)
continue;
const bool FAST = true; const bool FAST = true;
for(auto & target : temp.findPotentialTargets(FAST)) for(auto & target : temp.findPotentialTargets(FAST))
@ -573,7 +686,15 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
auto & ps = possibleCasts[i]; auto & ps = possibleCasts[i];
#if BATTLE_TRACE_LEVEL >= 1 #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 #endif
auto state = std::make_shared<HypotheticBattle>(env.get(), cb->getBattle(battleID)); 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); spells::BattleCast cast(state.get(), hero, spells::Mode::HERO, ps.spell);
cast.castEval(state->getServerCallback(), ps.dest); cast.castEval(state->getServerCallback(), ps.dest);
auto allUnits = state->battleGetUnitsIf([](const battle::Unit * u) -> bool { return true; }); auto allUnits = state->battleGetUnitsIf([](const battle::Unit * u) -> bool { return u->isValidTarget(); });
auto needFullEval = vstd::contains_if(allUnits, [&](const battle::Unit * u) -> bool auto needFullEval = vstd::contains_if(allUnits, [&](const battle::Unit * u) -> bool
{ {
@ -591,40 +712,57 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
DamageCache safeCopy = damageCache; DamageCache safeCopy = damageCache;
DamageCache innerCache(&safeCopy); DamageCache innerCache(&safeCopy);
innerCache.buildDamageCache(state, side); innerCache.buildDamageCache(state, side);
if(needFullEval || !cachedAttack) if(cachedAttack.ap && cachedAttack.waited)
{
state->makeWait(activeStack);
}
if(needFullEval || !cachedAttack.ap)
{ {
#if BATTLE_TRACE_LEVEL >= 1 #if BATTLE_TRACE_LEVEL >= 1
logAi->trace("Full evaluation is started due to stack speed affected."); logAi->trace("Full evaluation is started due to stack speed affected.");
#endif #endif
PotentialTargets innerTargets(activeStack, innerCache, state); 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()) if(!innerTargets.possibleAttacks.empty())
{ {
innerEvaluator.updateReachabilityMap(state);
auto newStackAction = innerEvaluator.findBestTarget(activeStack, innerTargets, innerCache, state); auto newStackAction = innerEvaluator.findBestTarget(activeStack, innerTargets, innerCache, state);
ps.value = newStackAction.score; ps.value = std::max(moveTarget.score, newStackAction.score);
} }
else else
{ {
ps.value = 0; ps.value = moveTarget.score;
} }
} }
else 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) for(const auto & unit : allUnits)
{ {
if (!unit->isValidTarget()) if(!unit->isValidTarget(true))
continue; continue;
auto newHealth = unit->getAvailableHealth(); auto newHealth = unit->getAvailableHealth();
auto oldHealth = vstd::find_or(healthOfStack, unit->unitId(), 0); // old health value may not exist for newly summoned units 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( auto dpsReduce = AttackPossibility::calculateDamageReduce(
nullptr, nullptr,
originalDefender && originalDefender->alive() ? originalDefender : unit, originalDefender && originalDefender->alive() ? originalDefender : unit,
damage, damage,
innerCache, innerCache,
state); state);
@ -645,23 +783,49 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
if(ourUnit * goodEffect == 1) 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; continue;
ps.value += dpsReduce * scoreEvaluator.getPositiveEffectMultiplier(); ps.value += dpsReduce * scoreEvaluator.getPositiveEffectMultiplier();
} }
else else
ps.value -= dpsReduce * scoreEvaluator.getNegativeEffectMultiplier(); // discourage AI making collateral damage with spells
ps.value -= 4 * dpsReduce * scoreEvaluator.getNegativeEffectMultiplier();
#if BATTLE_TRACE_LEVEL >= 1 #if BATTLE_TRACE_LEVEL >= 1
logAi->trace( // Ensure ps.dest is not empty before accessing the first element
"Spell affects %s (%d), dps: %2f", if (!ps.dest.empty())
unit->creatureId().toCreature()->getNameSingularTranslated(), {
unit->getCount(), logAi->trace(
dpsReduce); "Spell %s to %d affects %s (%d), dps: %2f oldHealth: %d newHealth: %d",
ps.spell->getNameTranslated(),
ps.dest.at(0).hexValue.hex, // Safe to access .at(0) now
unit->creatureId().toCreature()->getNameSingularTranslated(),
unit->getCount(),
dpsReduce,
oldHealth,
newHealth);
}
else
{
// Handle the case where ps.dest is empty
logAi->trace(
"Spell %s has no destination, affects %s (%d), dps: %2f oldHealth: %d newHealth: %d",
ps.spell->getNameTranslated(),
unit->creatureId().toCreature()->getNameSingularTranslated(),
unit->getCount(),
dpsReduce,
oldHealth,
newHealth);
}
#endif #endif
} }
} }
#if BATTLE_TRACE_LEVEL >= 1 #if BATTLE_TRACE_LEVEL >= 1
logAi->trace("Total score: %2f", ps.value); logAi->trace("Total score: %2f", ps.value);
#endif #endif
@ -672,13 +836,12 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
LOGFL("Evaluation took %d ms", timer.getDiff()); LOGFL("Evaluation took %d ms", timer.getDiff());
auto pscValue = [](const PossibleSpellcast &ps) -> float auto castToPerform = *vstd::maxElementByFun(possibleCasts, [](const PossibleSpellcast & ps) -> float
{ {
return ps.value; return ps.value;
}; });
auto castToPerform = *vstd::maxElementByFun(possibleCasts, pscValue);
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); LOGFL("Best spell is %s (value %d). Will cast.", castToPerform.spell->getNameTranslated() % castToPerform.value);
BattleAction spellcast; BattleAction spellcast;
@ -686,7 +849,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
spellcast.spell = castToPerform.spell->id; spellcast.spell = castToPerform.spell->id;
spellcast.setTarget(castToPerform.dest); spellcast.setTarget(castToPerform.dest);
spellcast.side = side; spellcast.side = side;
spellcast.stackNumber = (!side) ? -1 : -2; spellcast.stackNumber = -1;
cb->battleMakeSpellAction(battleID, spellcast); cb->battleMakeSpellAction(battleID, spellcast);
activeActionMade = true; activeActionMade = true;

View File

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

View File

@ -9,16 +9,17 @@
*/ */
#include "StdInc.h" #include "StdInc.h"
#include "BattleExchangeVariant.h" #include "BattleExchangeVariant.h"
#include "BattleEvaluator.h"
#include "../../lib/CStack.h" #include "../../lib/CStack.h"
AttackerValue::AttackerValue() AttackerValue::AttackerValue()
: value(0), : value(0),
isRetalitated(false) isRetaliated(false)
{ {
} }
MoveTarget::MoveTarget() MoveTarget::MoveTarget()
: positions(), cachedAttack(), score(EvaluationResult::INEFFECTIVE_SCORE), scorePerTurn(EvaluationResult::INEFFECTIVE_SCORE) : positions(), cachedAttack(), score(EvaluationResult::INEFFECTIVE_SCORE)
{ {
turnsToRich = 1; turnsToRich = 1;
} }
@ -28,102 +29,97 @@ float BattleExchangeVariant::trackAttack(
std::shared_ptr<HypotheticBattle> hb, std::shared_ptr<HypotheticBattle> hb,
DamageCache & damageCache) DamageCache & damageCache)
{ {
if(!ap.attackerState)
{
logAi->trace("Skipping fake ap attack");
return 0;
}
auto attacker = hb->getForUpdate(ap.attack.attacker->unitId()); auto attacker = hb->getForUpdate(ap.attack.attacker->unitId());
const std::string cachingStringBlocksRetaliation = "type_BLOCKS_RETALIATION"; float attackValue = ap.attackValue();
static const auto selectorBlocksRetaliation = Selector::type()(BonusType::BLOCKS_RETALIATION);
const bool counterAttacksBlocked = attacker->hasBonus(selectorBlocksRetaliation, cachingStringBlocksRetaliation);
float attackValue = 0;
auto affectedUnits = ap.affectedUnits; auto affectedUnits = ap.affectedUnits;
dpsScore.ourDamageReduce += ap.attackerDamageReduce + ap.collateralDamageReduce;
dpsScore.enemyDamageReduce += ap.defenderDamageReduce + ap.shootersBlockedDmg;
attackerValue[attacker->unitId()].value = attackValue;
affectedUnits.push_back(ap.attackerState); affectedUnits.push_back(ap.attackerState);
for(auto affectedUnit : affectedUnits) for(auto affectedUnit : affectedUnits)
{ {
auto unitToUpdate = hb->getForUpdate(affectedUnit->unitId()); 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->unitSide() == attacker->unitSide())
{ {
if(unitToUpdate->unitId() == attacker->unitId()) if(unitToUpdate->unitId() == attacker->unitId())
{ {
auto defender = hb->getForUpdate(ap.attack.defender->unitId()); unitToUpdate->afterAttack(ap.attack.shooting, false);
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);
#if BATTLE_TRACE_LEVEL>=1 #if BATTLE_TRACE_LEVEL>=1
logAi->trace( logAi->trace(
"%s -> %s, ap retalitation, %s, dps: %2f, score: %2f", "%s -> %s, ap retaliation, %s, dps: %lld",
defender->getDescription(), hb->getForUpdate(ap.attack.defender->unitId())->getDescription(),
unitToUpdate->getDescription(), ap.attack.attacker->getDescription(),
ap.attack.shooting ? "shot" : "mellee", ap.attack.shooting ? "shot" : "mellee",
retaliationDamage, damageDealt);
attackerDamageReduce);
#endif #endif
} }
else 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 #if BATTLE_TRACE_LEVEL>=1
logAi->trace( logAi->trace(
"%s -> %s, ap collateral, %s, dps: %2f, score: %2f", "%s, ap collateral, dps: %lld",
attacker->getDescription(),
unitToUpdate->getDescription(), unitToUpdate->getDescription(),
ap.attack.shooting ? "shot" : "mellee", damageDealt);
collateralDamage,
collateralDamageReduce);
#endif #endif
} }
} }
else else
{ {
int64_t attackDamage = damageCache.getDamage(attacker.get(), unitToUpdate.get(), hb); if(unitToUpdate->unitId() == ap.attack.defender->unitId())
float defenderDamageReduce = AttackPossibility::calculateDamageReduce(attacker.get(), unitToUpdate.get(), attackDamage, damageCache, hb); {
if(unitToUpdate->ableToRetaliate() && !affectedUnit->ableToRetaliate())
attackValue += defenderDamageReduce; {
dpsScore.enemyDamageReduce += defenderDamageReduce; unitToUpdate->afterAttack(ap.attack.shooting, true);
attackerValue[attacker->unitId()].value += defenderDamageReduce; }
unitToUpdate->damage(attackDamage);
#if BATTLE_TRACE_LEVEL>=1 #if BATTLE_TRACE_LEVEL>=1
logAi->trace( logAi->trace(
"%s -> %s, ap attack, %s, dps: %2f, score: %2f", "%s -> %s, ap attack, %s, dps: %lld",
attacker->getDescription(), attacker->getDescription(),
unitToUpdate->getDescription(), ap.attack.defender->getDescription(),
ap.attack.shooting ? "shot" : "mellee", ap.attack.shooting ? "shot" : "mellee",
attackDamage, damageDealt);
defenderDamageReduce);
#endif #endif
}
else
{
#if BATTLE_TRACE_LEVEL>=1
logAi->trace(
"%s, ap enemy collateral, dps: %lld",
unitToUpdate->getDescription(),
damageDealt);
#endif
}
} }
} }
#if BATTLE_TRACE_LEVEL >= 1 #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 #endif
attackValue += ap.shootersBlockedDmg;
dpsScore.enemyDamageReduce += ap.shootersBlockedDmg;
attacker->afterAttack(ap.attack.shooting, false);
return attackValue; return attackValue;
} }
@ -185,7 +181,7 @@ float BattleExchangeVariant::trackAttack(
if(isOurAttack) if(isOurAttack)
{ {
dpsScore.ourDamageReduce += attackerDamageReduce; dpsScore.ourDamageReduce += attackerDamageReduce;
attackerValue[attacker->unitId()].isRetalitated = true; attackerValue[attacker->unitId()].isRetaliated = true;
} }
else else
{ {
@ -218,7 +214,8 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
const battle::Unit * activeStack, const battle::Unit * activeStack,
PotentialTargets & targets, PotentialTargets & targets,
DamageCache & damageCache, DamageCache & damageCache,
std::shared_ptr<HypotheticBattle> hb) std::shared_ptr<HypotheticBattle> hb,
bool siegeDefense)
{ {
EvaluationResult result(targets.bestAction()); EvaluationResult result(targets.bestAction());
@ -230,13 +227,15 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
auto hbWaited = std::make_shared<HypotheticBattle>(env.get(), hb); auto hbWaited = std::make_shared<HypotheticBattle>(env.get(), hb);
hbWaited->getForUpdate(activeStack->unitId())->waiting = true; hbWaited->makeWait(activeStack);
hbWaited->getForUpdate(activeStack->unitId())->waitedThisTurn = true;
updateReachabilityMap(hbWaited); updateReachabilityMap(hbWaited);
for(auto & ap : targets.possibleAttacks) for(auto & ap : targets.possibleAttacks)
{ {
if (siegeDefense && !hb->battleIsInsideWalls(ap.from))
continue;
float score = evaluateExchange(ap, 0, targets, damageCache, hbWaited); float score = evaluateExchange(ap, 0, targets, damageCache, hbWaited);
if(score > result.score) if(score > result.score)
@ -259,6 +258,7 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
updateReachabilityMap(hb); updateReachabilityMap(hb);
if(result.bestAttack.attack.shooting if(result.bestAttack.attack.shooting
&& !result.bestAttack.defenderDead
&& !activeStack->waited() && !activeStack->waited()
&& hb->battleHasShootingPenalty(activeStack, result.bestAttack.dest)) && hb->battleHasShootingPenalty(activeStack, result.bestAttack.dest))
{ {
@ -268,9 +268,13 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
for(auto & ap : targets.possibleAttacks) 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.score = score;
result.bestAttack = ap; result.bestAttack = ap;
@ -285,6 +289,36 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
return result; 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( MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
const battle::Unit * activeStack, const battle::Unit * activeStack,
PotentialTargets & targets, PotentialTargets & targets,
@ -294,6 +328,8 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
MoveTarget result; MoveTarget result;
BattleExchangeVariant ev; BattleExchangeVariant ev;
logAi->trace("Find move towards unreachable. Enemies count %d", targets.unreachableEnemies.size());
if(targets.unreachableEnemies.empty()) if(targets.unreachableEnemies.empty())
return result; return result;
@ -304,17 +340,17 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
updateReachabilityMap(hb); 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) for(const battle::Unit * enemy : targets.unreachableEnemies)
{ {
std::vector<const battle::Unit *> adjacentStacks = getAdjacentUnits(enemy); logAi->trace(
auto closestStack = *vstd::minElementByFun(adjacentStacks, [&](const battle::Unit * u) -> int64_t "Checking movement towards %d of %s",
{ enemy->getCount(),
return dists.distToNearestNeighbour(activeStack, u) * 100000 - activeStack->getTotalHealth(); enemy->creatureId().toCreature()->getNameSingularTranslated());
});
auto distance = dists.distToNearestNeighbour(activeStack, closestStack); auto distance = dists.distToNearestNeighbour(activeStack, enemy);
if(distance >= GameConstants::BFIELD_SIZE) if(distance >= GameConstants::BFIELD_SIZE)
continue; continue;
@ -322,31 +358,109 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
if(distance <= speed) if(distance <= speed)
continue; 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 turnsToRich = (distance - 1) / speed + 1;
auto hexes = closestStack->getSurroundingHexes(); auto hexes = enemy->getSurroundingHexes();
auto enemySpeed = closestStack->getMovementRange(); auto enemySpeed = enemy->getMovementRange();
auto speedRatio = speed / static_cast<float>(enemySpeed); auto speedRatio = speed / static_cast<float>(enemySpeed);
auto multiplier = speedRatio > 1 ? 1 : speedRatio; auto multiplier = (speedRatio > 1 ? 1 : speedRatio) * penaltyMultiplier;
if(enemy->canShoot()) for(auto & hex : hexes)
multiplier *= 1.5f;
for(auto hex : hexes)
{ {
// FIXME: provide distance info for Jousting bonus // FIXME: provide distance info for Jousting bonus
auto bai = BattleAttackInfo(activeStack, closestStack, 0, cb->battleCanShoot(activeStack)); auto bai = BattleAttackInfo(activeStack, enemy, 0, cb->battleCanShoot(activeStack));
auto attack = AttackPossibility::evaluate(bai, hex, damageCache, hb); auto attack = AttackPossibility::evaluate(bai, hex, damageCache, hb);
attack.shootersBlockedDmg = 0; // we do not want to count on it, it is not for sure attack.shootersBlockedDmg = 0; // we do not want to count on it, it is not for sure
auto score = calculateExchange(attack, turnsToRich, targets, damageCache, hb); 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.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.cachedAttack = attack;
result.turnsToRich = turnsToRich; result.turnsToRich = turnsToRich;
} }
@ -390,7 +504,8 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits(
const AttackPossibility & ap, const AttackPossibility & ap,
uint8_t turn, uint8_t turn,
PotentialTargets & targets, PotentialTargets & targets,
std::shared_ptr<HypotheticBattle> hb) const std::shared_ptr<HypotheticBattle> hb,
std::vector<const battle::Unit *> additionalUnits) const
{ {
ReachabilityData result; ReachabilityData result;
@ -398,13 +513,29 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits(
if(!ap.attack.shooting) hexes.push_back(ap.from); 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) for(auto hex : hexes)
{ {
vstd::concatenate(allReachableUnits, turn == 0 ? reachabilityMap.at(hex) : getOneTurnReachableUnits(turn, hex)); 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); vstd::removeDuplicates(allReachableUnits);
auto copy = allReachableUnits; auto copy = allReachableUnits;
@ -440,7 +571,7 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits(
for(auto unit : allReachableUnits) for(auto unit : allReachableUnits)
{ {
auto accessible = !unit->canShoot(); auto accessible = !unit->canShoot() || vstd::contains(additionalUnits, unit);
if(!accessible) if(!accessible)
{ {
@ -464,14 +595,14 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits(
for(auto unit : turnOrder[turn]) for(auto unit : turnOrder[turn])
{ {
if(vstd::contains(allReachableUnits, unit)) 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 vstd::erase_if(result.units[turn], [&](const battle::Unit * u) -> bool
{ {
return !hb->battleGetUnitByID(u->unitId())->alive(); return !hb->battleGetUnitByID(u->unitId())->alive();
}); });
}
return result; return result;
} }
@ -502,13 +633,14 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
uint8_t turn, uint8_t turn,
PotentialTargets & targets, PotentialTargets & targets,
DamageCache & damageCache, 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 #if BATTLE_TRACE_LEVEL>=1
logAi->trace("Battle exchange at %d", ap.attack.shooting ? ap.dest.hex : ap.from.hex); logAi->trace("Battle exchange at %d", ap.attack.shooting ? ap.dest.hex : ap.from.hex);
#endif #endif
if(cb->battleGetMySide() == BattlePerspective::LEFT_SIDE if(cb->battleGetMySide() == BattleSide::LEFT_SIDE
&& cb->battleGetGateState() == EGateState::BLOCKED && cb->battleGetGateState() == EGateState::BLOCKED
&& ap.attack.defender->coversPos(BattleHex::GATE_BRIDGE)) && ap.attack.defender->coversPos(BattleHex::GATE_BRIDGE))
{ {
@ -521,7 +653,7 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
if(hb->battleGetUnitByID(ap.attack.defender->unitId())->alive()) if(hb->battleGetUnitByID(ap.attack.defender->unitId())->alive())
enemyStacks.push_back(ap.attack.defender); 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()) if(exchangeUnits.units.empty())
{ {
@ -531,22 +663,25 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
auto exchangeBattle = std::make_shared<HypotheticBattle>(env.get(), hb); auto exchangeBattle = std::make_shared<HypotheticBattle>(env.get(), hb);
BattleExchangeVariant v; BattleExchangeVariant v;
for(auto unit : exchangeUnits.units) for(int exchangeTurn = 0; exchangeTurn < exchangeUnits.units.size(); exchangeTurn++)
{ {
if(unit->isTurret()) for(auto unit : exchangeUnits.units.at(exchangeTurn))
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(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 #if BATTLE_TRACE_LEVEL
logAi->trace("Exchanging: %s", u->getDescription()); logAi->trace("Exchanging: %s", u->getDescription());
#endif #endif
}
} }
} }
@ -560,122 +695,166 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
bool canUseAp = true; 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); bool isMovingTurm = exchangeTurn < turn;
battle::Units & attackerQueue = isOur ? ourStacks : enemyStacks; int queueTurn = exchangeTurn >= exchangeUnits.units.size()
battle::Units & oppositeQueue = isOur ? enemyStacks : ourStacks; ? exchangeUnits.units.size() - 1
: exchangeTurn;
auto attacker = exchangeBattle->getForUpdate(activeUnit->unitId()); for(auto activeUnit : exchangeUnits.units.at(queueTurn))
if(!attacker->alive())
{ {
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 #if BATTLE_TRACE_LEVEL>=1
logAi->trace( "Attacker is dead"); logAi->trace("Attacker is dead");
#endif #endif
continue; 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);
} }
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 vstd::contains(exchangeUnits.shooters, u);
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()) 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 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 #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 #endif
continue; continue;
}
} }
} }
}
auto defender = exchangeBattle->getForUpdate(targetUnit->unitId()); auto defender = exchangeBattle->getForUpdate(targetUnit->unitId());
auto shooting = exchangeBattle->battleCanShoot(attacker.get()); const int totalAttacks = attacker->getTotalAttacks(shooting);
const int totalAttacks = attacker->getTotalAttacks(shooting);
if(canUseAp && activeUnit->unitId() == ap.attack.attacker->unitId() if(canUseAp && activeUnit->unitId() == ap.attack.attacker->unitId()
&& targetUnit->unitId() == ap.attack.defender->unitId()) && targetUnit->unitId() == ap.attack.defender->unitId())
{
v.trackAttack(ap, exchangeBattle, damageCache);
}
else
{
for(int i = 0; i < totalAttacks; i++)
{ {
v.trackAttack(attacker, defender, shooting, isOur, damageCache, exchangeBattle); v.trackAttack(ap, exchangeBattle, damageCache);
if(!attacker->alive() || !defender->alive())
break;
} }
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; exchangeBattle->nextRound();
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();
});
} }
// avoid blocking path for stronger stack by weaker stack // avoid blocking path for stronger stack by weaker stack
@ -687,11 +866,28 @@ BattleScore BattleExchangeEvaluator::calculateExchange(
for(auto hex : hexes) for(auto hex : hexes)
reachabilityMap[hex] = getOneTurnReachableUnits(turn, hex); 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 #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 #endif
return v.getScore(); return score;
} }
bool BattleExchangeEvaluator::canBeHitThisTurn(const AttackPossibility & ap) bool BattleExchangeEvaluator::canBeHitThisTurn(const AttackPossibility & ap)

View File

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

View File

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

View File

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

View File

@ -12,6 +12,7 @@
#include <vcmi/events/EventBus.h> #include <vcmi/events/EventBus.h>
#include "../../lib/battle/BattleLayout.h"
#include "../../lib/CStack.h" #include "../../lib/CStack.h"
#include "../../lib/ScriptHandler.h" #include "../../lib/ScriptHandler.h"
#include "../../lib/networkPacks/PacksForClientBattle.h" #include "../../lib/networkPacks/PacksForClientBattle.h"
@ -116,7 +117,7 @@ uint32_t StackWithBonuses::unitId() const
return id; return id;
} }
ui8 StackWithBonuses::unitSide() const BattleSide StackWithBonuses::unitSide() const
{ {
return side; return side;
} }
@ -132,10 +133,10 @@ SlotID StackWithBonuses::unitSlot() const
} }
TConstBonusListPtr StackWithBonuses::getAllBonuses(const CSelector & selector, const CSelector & limit, 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>(); 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) 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; return (damage.min + damage.max) / 2;
} }
std::vector<SpellID> HypotheticBattle::getUsedSpells(ui8 side) const std::vector<SpellID> HypotheticBattle::getUsedSpells(BattleSide side) const
{ {
// TODO // TODO
return {}; return {};
@ -479,10 +480,9 @@ int3 HypotheticBattle::getLocation() const
return int3(-1, -1, -1); return int3(-1, -1, -1);
} }
bool HypotheticBattle::isCreatureBank() const BattleLayout HypotheticBattle::getLayout() const
{ {
// TODO return subject->getBattle()->getLayout();
return false;
} }
int64_t HypotheticBattle::getTreeVersion() const int64_t HypotheticBattle::getTreeVersion() const
@ -502,10 +502,18 @@ ServerCallback * HypotheticBattle::getServerCallback()
return serverCallback.get(); 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_) HypotheticBattle::HypotheticServerCallback::HypotheticServerCallback(HypotheticBattle * owner_)
:owner(owner_) :owner(owner_)
{ {
} }
void HypotheticBattle::HypotheticServerCallback::complain(const std::string & problem) void HypotheticBattle::HypotheticServerCallback::complain(const std::string & problem)
@ -523,44 +531,44 @@ vstd::RNG * HypotheticBattle::HypotheticServerCallback::getRNG()
return &rngStub; 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()); 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) HypotheticBattle::HypotheticEnvironment::HypotheticEnvironment(HypotheticBattle * owner_, const Environment * upperEnvironment)

View File

@ -21,23 +21,43 @@
class HypotheticBattle; class HypotheticBattle;
///Fake random generator, used by AI to evaluate random server behavior ///Fake random generator, used by AI to evaluate random server behavior
class RNGStub : public vstd::RNG class RNGStub final : public vstd::RNG
{ {
public: public:
vstd::TRandI64 getInt64Range(int64_t lower, int64_t upper) override int nextInt() override
{ {
return [=]()->int64_t return 0;
{
return (lower + upper)/2;
};
} }
vstd::TRand getDoubleRange(double lower, double upper) override int nextBinomialInt(int coinsCount, double coinChance) override
{ {
return [=]()->double return coinsCount * coinChance;
{ }
return (lower + upper)/2;
}; 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; int32_t unitBaseAmount() const override;
uint32_t unitId() const override; uint32_t unitId() const override;
ui8 unitSide() const override; BattleSide unitSide() const override;
PlayerColor unitOwner() const override; PlayerColor unitOwner() const override;
SlotID unitSlot() const override; SlotID unitSlot() const override;
///IBonusBearer ///IBonusBearer
TConstBonusListPtr getAllBonuses(const CSelector & selector, const CSelector & limit, 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; int64_t getTreeVersion() const override;
@ -91,7 +111,7 @@ private:
const CCreature * type; const CCreature * type;
ui32 baseAmount; ui32 baseAmount;
uint32_t id; uint32_t id;
ui8 side; BattleSide side;
PlayerColor player; PlayerColor player;
SlotID slot; SlotID slot;
}; };
@ -138,12 +158,19 @@ public:
uint32_t nextUnitId() const override; uint32_t nextUnitId() const override;
int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const override; int64_t getActualDamage(const DamageRange & damage, int32_t attackerCount, vstd::RNG & rng) const override;
std::vector<SpellID> getUsedSpells(ui8 side) const override; std::vector<SpellID> getUsedSpells(BattleSide side) const override;
int3 getLocation() const override; int3 getLocation() const override;
bool isCreatureBank() const override; BattleLayout getLayout() const override;
int64_t getTreeVersion() const; int64_t getTreeVersion() const;
void makeWait(const battle::Unit * activeStack);
void resetActiveUnit()
{
activeUnitId = -1;
}
#if SCRIPTING_ENABLED #if SCRIPTING_ENABLED
scripting::Pool * getContextPool() const override; scripting::Pool * getContextPool() const override;
#endif #endif
@ -162,15 +189,15 @@ private:
vstd::RNG * getRNG() override; vstd::RNG * getRNG() override;
void apply(CPackForClient * pack) override; void apply(CPackForClient & pack) override;
void apply(BattleLogMessage * pack) override; void apply(BattleLogMessage & pack) override;
void apply(BattleStackMoved * pack) override; void apply(BattleStackMoved & pack) override;
void apply(BattleUnitsChanged * pack) override; void apply(BattleUnitsChanged & pack) override;
void apply(SetStackEffect * pack) override; void apply(SetStackEffect & pack) override;
void apply(StacksInjured * pack) override; void apply(StacksInjured & pack) override;
void apply(BattleObstaclesChanged * pack) override; void apply(BattleObstaclesChanged & pack) override;
void apply(CatapultAttack * pack) override; void apply(CatapultAttack & pack) override;
private: private:
HypotheticBattle * owner; HypotheticBattle * owner;
RNGStub rngStub; 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; std::array<int, GameConstants::BFIELD_SIZE> sufferedDamage;
ThreatMap(const CStack *Endangered); 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) option(FORCE_BUNDLED_FL "Force to use FuzzyLite included into VCMI's source tree" OFF)
endif() 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 #FuzzyLite uses MSVC pragmas in headers, so, we need to disable -Wunknown-pragmas
if(MINGW) if(MINGW)
add_compile_options(-Wno-unknown-pragmas) add_compile_options(-Wno-unknown-pragmas)

View File

@ -14,14 +14,6 @@
#include "../../lib/CStack.h" #include "../../lib/CStack.h"
#include "../../lib/battle/BattleAction.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) void CEmptyAI::initGameInterface(std::shared_ptr<Environment> ENV, std::shared_ptr<CCallback> CB)
{ {
cb = CB; cb = CB;

View File

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

View File

@ -16,9 +16,7 @@
#include "../../lib/CThreadHelper.h" #include "../../lib/CThreadHelper.h"
#include "../../lib/GameConstants.h" #include "../../lib/GameConstants.h"
#include "../../lib/VCMI_Lib.h" #include "../../lib/VCMI_Lib.h"
#include "../../lib/CBuildingHandler.h"
#include "../../lib/CCreatureHandler.h" #include "../../lib/CCreatureHandler.h"
#include "../../lib/CTownHandler.h"
#include "../../lib/mapObjects/MiscObjects.h" #include "../../lib/mapObjects/MiscObjects.h"
#include "../../lib/spells/CSpellHandler.h" #include "../../lib/spells/CSpellHandler.h"
#include "Pathfinding/AIPathfinder.h" #include "Pathfinding/AIPathfinder.h"
@ -59,15 +57,6 @@ public:
void attemptedAnsweringQuery(QueryID queryID, int answerRequestID); void attemptedAnsweringQuery(QueryID queryID, int answerRequestID);
void receivedAnswerConfirmation(int answerRequestID, int result); void receivedAnswerConfirmation(int answerRequestID, int result);
void heroVisit(const CGObjectInstance * obj, bool started); 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 // The gateway is responsible for AI events handling. Copied from VCAI.h and refined a bit
@ -104,7 +93,7 @@ public:
AIGateway(); AIGateway();
virtual ~AIGateway(); virtual ~AIGateway();
//TODO: extract to apropriate goals //TODO: extract to appropriate goals
void tryRealize(Goals::DigAtTile & g); void tryRealize(Goals::DigAtTile & g);
void tryRealize(Goals::Trade & 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 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 showTeleportDialog(const CGHeroInstance * hero, TeleportChannelID channel, TTeleportExitsList exits, bool impassable, QueryID askID) override;
void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector<ObjectInstanceID> & objects) override; void showMapObjectSelectDialog(QueryID askID, const Component & icon, const MetaString & title, const MetaString & description, const std::vector<ObjectInstanceID> & objects) override;
void saveGame(BinarySerializer & h) override; //saving
void loadGame(BinaryDeserializer & h) override; //loading
void finish() override; void finish() override;
void availableCreaturesChanged(const CGDwelling * town) override; void availableCreaturesChanged(const CGDwelling * town) override;
@ -169,7 +156,7 @@ public:
void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override; void showWorldViewEx(const std::vector<ObjectPosInfo> & objectPositions, bool showTerrain) override;
std::optional<BattleAction> makeSurrenderRetreatDecision(const BattleID & battleID, const BattleStateInfoForRetreat & battleState) 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 battleEnd(const BattleID & battleID, const BattleResult * br, QueryID queryID) override;
void makeTurn(); void makeTurn();
@ -202,17 +189,6 @@ public:
void answerQuery(QueryID queryID, int selection); void answerQuery(QueryID queryID, int selection);
//special function that can be called ONLY from game events handling thread and will send request ASAP //special function that can be called ONLY from game events handling thread and will send request ASAP
void requestActionASAP(std::function<void()> whatToDo); 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/UnlockGuard.h"
#include "../../lib/CConfigHandler.h" #include "../../lib/CConfigHandler.h"
#include "../../lib/CHeroHandler.h"
#include "../../lib/mapObjects/MapObjects.h" #include "../../lib/mapObjects/MapObjects.h"
#include "../../lib/mapping/CMapDefines.h" #include "../../lib/mapping/CMapDefines.h"
#include "../../lib/gameState/QuestInfo.h" #include "../../lib/gameState/QuestInfo.h"
#include "../../lib/GameSettings.h" #include "../../lib/IGameSettings.h"
#include <vcmi/CreatureService.h> #include <vcmi/CreatureService.h>
@ -147,21 +146,21 @@ bool HeroPtr::operator==(const HeroPtr & rhs) const
return h == rhs.get(true); return h == rhs.get(true);
} }
bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet * heroArmy, uint64_t dangerStrength) bool isSafeToVisit(const CGHeroInstance * h, const CCreatureSet * heroArmy, uint64_t dangerStrength, float safeAttackRatio)
{ {
const ui64 heroStrength = h->getFightingStrength() * heroArmy->getArmyStrength(); const ui64 heroStrength = h->getHeroStrength() * heroArmy->getArmyStrength();
if(dangerStrength) if(dangerStrength)
{ {
return heroStrength / SAFE_ATTACK_CONSTANT > dangerStrength; return heroStrength > dangerStrength * safeAttackRatio;
} }
return true; //there's no danger return true; //there's no danger
} }
bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength) bool isSafeToVisit(const CGHeroInstance * h, uint64_t dangerStrength, float safeAttackRatio)
{ {
return isSafeToVisit(h, h, dangerStrength); return isSafeToVisit(h, h, dangerStrength, safeAttackRatio);
} }
bool isObjectRemovable(const CGObjectInstance * obj) bool isObjectRemovable(const CGObjectInstance * obj)
@ -194,7 +193,7 @@ bool canBeEmbarkmentPoint(const TerrainTile * t, bool fromWater)
{ {
// TODO: Such information should be provided by pathfinder // TODO: Such information should be provided by pathfinder
// Tile must be free or with unoccupied boat // Tile must be free or with unoccupied boat
if(!t->blocked) if(!t->blocked())
{ {
return true; return true;
} }
@ -268,8 +267,8 @@ bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2)
bool compareArtifacts(const CArtifactInstance * a1, const CArtifactInstance * a2) bool compareArtifacts(const CArtifactInstance * a1, const CArtifactInstance * a2)
{ {
auto art1 = a1->artType; auto art1 = a1->getType();
auto art2 = a2->artType; auto art2 = a2->getType();
if(art1->getPrice() == art2->getPrice()) if(art1->getPrice() == art2->getPrice())
return art1->valOfBonuses(BonusType::PRIMARY_SKILL) > art2->valOfBonuses(BonusType::PRIMARY_SKILL); 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()) 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++; duplicatingSlots++;
} }
@ -388,7 +387,7 @@ bool shouldVisit(const Nullkiller * ai, const CGHeroInstance * h, const CGObject
{ {
for(auto slot : h->Slots()) for(auto slot : h->Slots())
{ {
if(slot.second->type->hasUpgrades()) if(slot.second->getType()->hasUpgrades())
return true; //TODO: check price? return true; //TODO: check price?
} }
return false; return false;
@ -430,9 +429,16 @@ bool shouldVisit(const Nullkiller * ai, const CGHeroInstance * h, const CGObject
return false; return false;
} }
if(obj->wasVisited(h)) //it must pointer to hero instance, heroPtr calls function wasVisited(ui8 player); if(obj->wasVisited(h))
return false; 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; return true;
} }
@ -441,9 +447,9 @@ bool townHasFreeTavern(const CGTownInstance * town)
if(!town->hasBuilt(BuildingID::TAVERN)) return false; if(!town->hasBuilt(BuildingID::TAVERN)) return false;
if(!town->visitingHero) return true; 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) uint64_t getHeroArmyStrengthWithCommander(const CGHeroInstance * hero, const CCreatureSet * heroArmy)

View File

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

View File

@ -13,6 +13,7 @@
#include "../Engine/Nullkiller.h" #include "../Engine/Nullkiller.h"
#include "../../../CCallback.h" #include "../../../CCallback.h"
#include "../../../lib/mapObjects/MapObjects.h" #include "../../../lib/mapObjects/MapObjects.h"
#include "../../../lib/IGameSettings.h"
#include "../../../lib/GameConstants.h" #include "../../../lib/GameConstants.h"
namespace NKAI namespace NKAI
@ -90,7 +91,7 @@ std::vector<SlotInfo> ArmyManager::getSortedSlots(const CCreatureSet * target, c
{ {
for(auto & i : armyPtr->Slots()) 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]; auto & slotInfp = creToPower[cre];
slotInfp.creature = cre; slotInfp.creature = cre;
@ -144,7 +145,7 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
for(auto & slot : sortedSlots) for(auto & slot : sortedSlots)
{ {
alignmentMap[slot.creature->getFaction()] += slot.power; alignmentMap[slot.creature->getFactionID()] += slot.power;
} }
std::set<FactionID> allowedFactions; std::set<FactionID> allowedFactions;
@ -152,16 +153,6 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
uint64_t armyValue = 0; uint64_t armyValue = 0;
TemporaryArmy newArmyInstance; TemporaryArmy newArmyInstance;
auto bonusModifiers = armyCarrier->getBonuses(Selector::type()(BonusType::MORALE));
for(auto bonus : *bonusModifiers)
{
// army bonuses will change and object bonuses are temporary
if(bonus->source != BonusSource::ARMY && bonus->source != BonusSource::OBJECT_INSTANCE && bonus->source != BonusSource::OBJECT_TYPE)
{
newArmyInstance.addNewBonus(std::make_shared<Bonus>(*bonus));
}
}
while(allowedFactions.size() < alignmentMap.size()) while(allowedFactions.size() < alignmentMap.size())
{ {
@ -178,7 +169,7 @@ std::vector<SlotInfo> ArmyManager::getBestArmy(const IBonusBearer * armyCarrier,
for(auto & slot : sortedSlots) 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()); 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 morale = slot.second->moraleVal();
auto multiplier = 1.0f; auto multiplier = 1.0f;
const float BadMoraleChance = 0.083f; const auto & badMoraleDice = cb->getSettings().getVector(EGameSettings::COMBAT_BAD_MORALE_DICE);
const float HighMoraleChance = 0.04f; const auto & highMoraleDice = cb->getSettings().getVector(EGameSettings::COMBAT_GOOD_MORALE_DICE);
if(morale < 0) if(morale < 0 && !badMoraleDice.empty())
{ {
multiplier += morale * BadMoraleChance; size_t diceIndex = std::min<size_t>(badMoraleDice.size(), -morale) - 1;
multiplier -= 1.0 / badMoraleDice.at(diceIndex);
} }
else if(morale > 0) else if(morale > 0 && !highMoraleDice.empty())
{ {
multiplier += morale * HighMoraleChance; size_t diceIndex = std::min<size_t>(highMoraleDice.size(), morale) - 1;
multiplier += 1.0 / highMoraleDice.at(diceIndex);
} }
newValue += multiplier * slot.second->getPower(); newValue += multiplier * slot.second->getPower();
@ -316,6 +309,8 @@ std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
? dynamic_cast<const CGTownInstance *>(dwelling) ? dynamic_cast<const CGTownInstance *>(dwelling)
: nullptr; : nullptr;
std::set<SlotID> alreadyDisbanded;
for(int i = dwelling->creatures.size() - 1; i >= 0; i--) for(int i = dwelling->creatures.size() - 1; i >= 0; i--)
{ {
auto ci = infoFromDC(dwelling->creatures[i]); auto ci = infoFromDC(dwelling->creatures[i]);
@ -329,18 +324,71 @@ std::vector<creInfo> ArmyManager::getArmyAvailableToBuy(
if(!ci.count) continue; 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); 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(!hero->hasStackAtSlot(dst)) //need another new slot for this stack
{ {
if(!freeHeroSlots) //no more place for stacks if(!freeHeroSlots) // No free slots; consider replacing
continue; {
// 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 else
{
freeHeroSlots--; //new slot will be occupied freeHeroSlots--; //new slot will be occupied
}
} }
vstd::amin(ci.count, availableRes / ci.creID.toCreature()->getFullRecruitCost()); //max count we can afford 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 ci.level = i; //this is important for Dungeon Summoning Portal
creaturesInDwellings.push_back(ci); creaturesInDwellings.push_back(ci);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -28,9 +28,6 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const
{ {
Goals::TGoalVec tasks; Goals::TGoalVec tasks;
if(ai->cb->getDate(Date::DAY) == 1)
return tasks;
auto heroes = cb->getHeroesInfo(); auto heroes = cb->getHeroesInfo();
if(heroes.empty()) if(heroes.empty())
@ -38,19 +35,23 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const
return tasks; return tasks;
} }
ai->dangerHitMap->updateHitMap();
for(auto town : cb->getTownsInfo()) for(auto town : cb->getTownsInfo())
{ {
uint8_t closestThreat = ai->dangerHitMap->getTileThreat(town->visitablePos()).fastestDanger.turn;
if (closestThreat >=2 && ai->buildAnalyzer->isGoldPressureHigh() && !town->hasBuilt(BuildingID::CITY_HALL) && cb->canBuildStructure(town, BuildingID::CITY_HALL) != EBuildingState::FORBIDDEN)
{
return tasks;
}
auto townArmyAvailableToBuy = ai->armyManager->getArmyAvailableToBuyAsCCreatureSet( auto townArmyAvailableToBuy = ai->armyManager->getArmyAvailableToBuyAsCCreatureSet(
town, town,
ai->getFreeResources()); ai->getFreeResources());
for(const CGHeroInstance * targetHero : heroes) for(const CGHeroInstance * targetHero : heroes)
{ {
if(ai->buildAnalyzer->isGoldPressureHigh() && !town->hasBuilt(BuildingID::CITY_HALL))
{
continue;
}
if(ai->heroManager->getHeroRole(targetHero) == HeroRole::MAIN) if(ai->heroManager->getHeroRole(targetHero) == HeroRole::MAIN)
{ {
auto reinforcement = ai->armyManager->howManyReinforcementsCanGet( auto reinforcement = ai->armyManager->howManyReinforcementsCanGet(
@ -63,7 +64,7 @@ Goals::TGoalVec BuyArmyBehavior::decompose(const Nullkiller * ai) const
if(reinforcement) 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()); logAi->trace("Path found %s", path.toString());
#endif #endif
if(nullkiller->dangerHitMap->enemyCanKillOurHeroesAlongThePath(path))
{
#if NKAI_TRACE_LEVEL >= 2
logAi->trace("Ignore path. Target hero can be killed by enemy. Our power %lld", path.getHeroStrength());
#endif
continue;
}
if(objToVisit && !force && !shouldVisit(nullkiller, path.targetHero, objToVisit)) if(objToVisit && !force && !shouldVisit(nullkiller, path.targetHero, objToVisit))
{ {
#if NKAI_TRACE_LEVEL >= 2 #if NKAI_TRACE_LEVEL >= 2
@ -87,6 +79,9 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(
auto hero = path.targetHero; auto hero = path.targetHero;
auto danger = path.getTotalDanger(); auto danger = path.getTotalDanger();
if (hero->getOwner() != nullkiller->playerID)
continue;
if(nullkiller->heroManager->getHeroRole(hero) == HeroRole::SCOUT if(nullkiller->heroManager->getHeroRole(hero) == HeroRole::SCOUT
&& (path.getTotalDanger() == 0 || path.turn() > 0) && (path.getTotalDanger() == 0 || path.turn() > 0)
&& path.exchangeCount > 1) && path.exchangeCount > 1)
@ -119,7 +114,7 @@ Goals::TGoalVec CaptureObjectsBehavior::getVisitGoals(
continue; continue;
} }
auto isSafe = isSafeToVisit(hero, path.heroArmy, danger); auto isSafe = isSafeToVisit(hero, path.heroArmy, danger, nullkiller->settings->getSafeAttackRatio());
#if NKAI_TRACE_LEVEL >= 2 #if NKAI_TRACE_LEVEL >= 2
logAi->trace( logAi->trace(
@ -212,7 +207,7 @@ void CaptureObjectsBehavior::decomposeObjects(
vstd::concatenate(tasksLocal, getVisitGoals(paths, nullkiller, objToVisit, specificObjects)); 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); 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))); tasks.push_back(Goals::sptr(Goals::ExchangeSwapTownHeroes(town, nullptr).setpriority(5)));
return true; return false;
} }
else if(ai->heroManager->getHeroRole(town->garrisonHero.get()) == HeroRole::MAIN) else if(ai->heroManager->getHeroRole(town->garrisonHero.get()) == HeroRole::MAIN)
{ {
@ -141,7 +141,7 @@ bool handleGarrisonHeroFromPreviousTurn(const CGTownInstance * town, Goals::TGoa
{ {
tasks.push_back(Goals::sptr(Goals::DismissHero(heroToDismiss).setpriority(5))); tasks.push_back(Goals::sptr(Goals::DismissHero(heroToDismiss).setpriority(5)));
return true; return false;
} }
} }
} }
@ -158,11 +158,10 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
threats.push_back(threatNode.fastestDanger); // no guarantee that fastest danger will be there threats.push_back(threatNode.fastestDanger); // no guarantee that fastest danger will be there
if(town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks, ai)) if (town->garrisonHero && handleGarrisonHeroFromPreviousTurn(town, tasks, ai))
{ {
return; return;
} }
if(!threatNode.fastestDanger.hero) if(!threatNode.fastestDanger.hero)
{ {
logAi->trace("No threat found for town %s", town->getNameTranslated()); logAi->trace("No threat found for town %s", town->getNameTranslated());
@ -240,7 +239,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
if(path.turn() <= threat.turn - 2) if(path.turn() <= threat.turn - 2)
{ {
#if NKAI_TRACE_LEVEL >= 1 #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(), town->getObjectName(),
path.targetHero->getObjectName()); path.targetHero->getObjectName());
#endif #endif
@ -250,6 +249,16 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
continue; continue;
} }
if (!path.targetHero->canBeMergedWith(*town))
{
#if NKAI_TRACE_LEVEL >= 1
logAi->trace("Can't merge armies of hero %s and town %s",
path.targetHero->getObjectName(),
town->getObjectName());
#endif
continue;
}
if(path.targetHero == town->visitingHero.get() && path.exchangeCount == 1) if(path.targetHero == town->visitingHero.get() && path.exchangeCount == 1)
{ {
#if NKAI_TRACE_LEVEL >= 1 #if NKAI_TRACE_LEVEL >= 1
@ -261,6 +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 // dismiss creatures we are not able to pick to be able to hide in garrison
if(town->garrisonHero if(town->garrisonHero
|| town->getUpperArmy()->stacksCount() == 0 || town->getUpperArmy()->stacksCount() == 0
|| path.targetHero->canBeMergedWith(*town)
|| (town->getUpperArmy()->getArmyStrength() < 500 && town->fortLevel() >= CGTownInstance::CITADEL)) || (town->getUpperArmy()->getArmyStrength() < 500 && town->fortLevel() >= CGTownInstance::CITADEL))
{ {
tasks.push_back( tasks.push_back(
@ -292,7 +302,7 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
continue; continue;
} }
if(threat.turn == 0 || (path.turn() <= threat.turn && path.getHeroStrength() * SAFE_ATTACK_CONSTANT >= threat.danger)) if(threat.turn == 0 || (path.turn() <= threat.turn && path.getHeroStrength() * ai->settings->getSafeAttackRatio() >= threat.danger))
{ {
if(ai->arePathHeroesLocked(path)) if(ai->arePathHeroesLocked(path))
{ {
@ -343,23 +353,14 @@ void DefenceBehavior::evaluateDefence(Goals::TGoalVec & tasks, const CGTownInsta
} }
else if(town->visitingHero && path.targetHero != town->visitingHero && !path.containsHero(town->visitingHero)) else if(town->visitingHero && path.targetHero != town->visitingHero && !path.containsHero(town->visitingHero))
{ {
if(town->garrisonHero) if(town->garrisonHero && town->garrisonHero != path.targetHero)
{ {
if(ai->heroManager->getHeroRole(town->visitingHero.get()) == HeroRole::SCOUT
&& town->visitingHero->getArmyStrength() < path.heroArmy->getArmyStrength() / 20)
{
if(path.turn() == 0)
sequence.push_back(sptr(DismissHero(town->visitingHero.get())));
}
else
{
#if NKAI_TRACE_LEVEL >= 1 #if NKAI_TRACE_LEVEL >= 1
logAi->trace("Cancel moving %s to defend town %s as the town has garrison hero", logAi->trace("Cancel moving %s to defend town %s as the town has garrison hero",
path.targetHero->getObjectName(), path.targetHero->getObjectName(),
town->getObjectName()); town->getObjectName());
#endif #endif
continue; continue;
}
} }
else if(path.turn() == 0) else if(path.turn() == 0)
{ {
@ -405,6 +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 void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitMapInfo & threat, const CGTownInstance * town, const Nullkiller * ai) const
{ {
if (threat.turn > 0 || town->garrisonHero || town->visitingHero)
return;
if(town->hasBuilt(BuildingID::TAVERN) if(town->hasBuilt(BuildingID::TAVERN)
&& ai->cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST) && ai->cb->getResourceAmount(EGameResID::GOLD) > GameConstants::HERO_GOLD_COST)
{ {
@ -415,6 +419,21 @@ void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitM
if(hero->getTotalStrength() < threat.danger) if(hero->getTotalStrength() < threat.danger)
continue; 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(); auto myHeroes = ai->cb->getHeroesInfo();
#if NKAI_TRACE_LEVEL >= 1 #if NKAI_TRACE_LEVEL >= 1
@ -451,7 +470,7 @@ void DefenceBehavior::evaluateRecruitingHero(Goals::TGoalVec & tasks, const HitM
} }
else if(ai->heroManager->heroCapReached()) else if(ai->heroManager->heroCapReached())
{ {
heroToDismiss = ai->heroManager->findWeakHeroToDismiss(hero->getArmyStrength()); heroToDismiss = ai->heroManager->findWeakHeroToDismiss(hero->getArmyStrength(), town);
if(!heroToDismiss) if(!heroToDismiss)
continue; continue;

View File

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

View File

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

View File

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

View File

@ -79,6 +79,15 @@ bool needToRecruitHero(const Nullkiller * ai, const CGTownInstance * startupTown
bool isGoldPile = dynamic_cast<const CGResource *>(obj) bool isGoldPile = dynamic_cast<const CGResource *>(obj)
&& dynamic_cast<const CGResource *>(obj)->resourceID() == EGameResID::GOLD; && 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 if(isGoldPile
|| obj->ID == Obj::TREASURE_CHEST || obj->ID == Obj::TREASURE_CHEST
|| obj->ID == Obj::CAMPFIRE || obj->ID == Obj::CAMPFIRE

View File

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

View File

@ -8,6 +8,7 @@ set(Nullkiller_SRCS
Pathfinding/Actions/QuestAction.cpp Pathfinding/Actions/QuestAction.cpp
Pathfinding/Actions/BuyArmyAction.cpp Pathfinding/Actions/BuyArmyAction.cpp
Pathfinding/Actions/BoatActions.cpp Pathfinding/Actions/BoatActions.cpp
Pathfinding/Actions/WhirlpoolAction.cpp
Pathfinding/Actions/TownPortalAction.cpp Pathfinding/Actions/TownPortalAction.cpp
Pathfinding/Actions/AdventureSpellCastMovementActions.cpp Pathfinding/Actions/AdventureSpellCastMovementActions.cpp
Pathfinding/Rules/AILayerTransitionRule.cpp Pathfinding/Rules/AILayerTransitionRule.cpp
@ -79,6 +80,7 @@ set(Nullkiller_HEADERS
Pathfinding/Actions/QuestAction.h Pathfinding/Actions/QuestAction.h
Pathfinding/Actions/BuyArmyAction.h Pathfinding/Actions/BuyArmyAction.h
Pathfinding/Actions/BoatActions.h Pathfinding/Actions/BoatActions.h
Pathfinding/Actions/WhirlpoolAction.h
Pathfinding/Actions/TownPortalAction.h Pathfinding/Actions/TownPortalAction.h
Pathfinding/Actions/AdventureSpellCastMovementActions.h Pathfinding/Actions/AdventureSpellCastMovementActions.h
Pathfinding/Rules/AILayerTransitionRule.h Pathfinding/Rules/AILayerTransitionRule.h
@ -155,11 +157,7 @@ else()
endif() endif()
target_include_directories(Nullkiller PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}) 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") vcmi_set_output_dir(Nullkiller "AI")
enable_pch(Nullkiller) 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 namespace NKAI
{ {
#define MIN_AI_STRENGTH (0.5f) //lower when combat AI gets smarter constexpr float MIN_AI_STRENGTH = 0.5f; //lower when combat AI gets smarter
#define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us
engineBase::engineBase() engineBase::engineBase()
{ {
@ -208,12 +207,6 @@ float TacticalAdvantageEngine::getTacticalAdvantage(const CArmedInstance * we, c
enemyFlyers->setValue(enemyStructure.flyers); enemyFlyers->setValue(enemyStructure.flyers);
enemySpeed->setValue(enemyStructure.maxSpeed); 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); const CGTownInstance * fort = dynamic_cast<const CGTownInstance *>(enemy);
if(fort) if(fort)
castleWalls->setValue(fort->fortLevel()); castleWalls->setValue(fort->fortLevel());

View File

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

View File

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

View File

@ -15,6 +15,8 @@
#include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h" #include "../../../lib/mapObjectConstructors/CObjectClassesHandler.h"
#include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h" #include "../../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
#include "../../../lib/mapObjects/MapObjects.h" #include "../../../lib/mapObjects/MapObjects.h"
#include "../../../lib/mapping/CMapDefines.h"
#include "../../../lib/RoadHandler.h"
#include "../../../lib/CCreatureHandler.h" #include "../../../lib/CCreatureHandler.h"
#include "../../../lib/VCMI_Lib.h" #include "../../../lib/VCMI_Lib.h"
#include "../../../lib/StartInfo.h" #include "../../../lib/StartInfo.h"
@ -33,11 +35,9 @@
namespace NKAI namespace NKAI
{ {
#define MIN_AI_STRENGHT (0.5f) //lower when combat AI gets smarter constexpr float MIN_CRITICAL_VALUE = 2.0f;
#define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us
const float MIN_CRITICAL_VALUE = 2.0f;
EvaluationContext::EvaluationContext(const Nullkiller * ai) EvaluationContext::EvaluationContext(const Nullkiller* ai)
: movementCost(0.0), : movementCost(0.0),
manaCost(0), manaCost(0),
danger(0), danger(0),
@ -51,9 +51,22 @@ EvaluationContext::EvaluationContext(const Nullkiller * ai)
heroRole(HeroRole::SCOUT), heroRole(HeroRole::SCOUT),
turn(0), turn(0),
strategicalValue(0), strategicalValue(0),
conquestValue(0),
evaluator(ai), evaluator(ai),
enemyHeroDangerRatio(0), enemyHeroDangerRatio(0),
armyGrowth(0) threat(0),
armyGrowth(0),
armyInvolvement(0),
defenseValue(0),
isDefend(false),
threatTurns(INT_MAX),
involvesSailing(false),
isTradeBuilding(false),
isExchange(false),
isArmyUpgrade(false),
isHero(false),
isEnemy(false),
explorePriority(0)
{ {
} }
@ -118,35 +131,17 @@ int32_t estimateTownIncome(CCallback * cb, const CGObjectInstance * target, cons
return booster * (town->hasFort() && town->tempOwner != PlayerColor::NEUTRAL ? booster * 500 : 250); 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); for(auto r : GameResID::ALL_RESOURCES())
CBankInfo * bankInfo = dynamic_cast<CBankInfo *>(objectInfo.get());
auto resources = bankInfo->getPossibleResourcesReward();
TResources result = TResources();
int sum = 0;
for(auto & reward : resources)
{ {
result += reward.data * reward.chance; if(res[r] > 0)
sum += reward.chance; result += r == EGameResID::GOLD ? res[r] : res[r] * 100;
} }
return sum > 1 ? result / sum : result; return 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;
} }
uint64_t getCreatureBankArmyReward(const CGObjectInstance * target, const CGHeroInstance * hero) 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) for (auto c : creatures)
{ {
//Only if hero has slot for this creature in the army //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) 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 /*else
{ {
@ -243,16 +238,16 @@ int getDwellingArmyCost(const CGObjectInstance * target)
auto creature = creLevel.second.back().toCreature(); auto creature = creLevel.second.back().toCreature();
auto creaturesAreFree = creature->getLevel() == 1; auto creaturesAreFree = creature->getLevel() == 1;
if(!creaturesAreFree) if(!creaturesAreFree)
cost += creature->getRecruitCost(EGameResID::GOLD) * creLevel.first; cost += creature->getFullRecruitCost().marketValue() * creLevel.first;
} }
} }
return cost; 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; return 1500;
auto statsValue = auto statsValue =
@ -267,8 +262,10 @@ static uint64_t evaluateArtifactArmyValue(const CArtifactInstance * art)
auto classValue = 0; auto classValue = 0;
switch(art->artType->aClass) switch(art->aClass)
{ {
case CArtifact::EartClass::ART_TREASURE:
//FALL_THROUGH
case CArtifact::EartClass::ART_MINOR: case CArtifact::EartClass::ART_MINOR:
classValue = 1000; classValue = 1000;
break; break;
@ -302,22 +299,15 @@ uint64_t RewardEvaluator::getArmyReward(
{ {
case Obj::HILL_FORT: case Obj::HILL_FORT:
return ai->armyManager->calculateCreaturesUpgrade(army, target, ai->cb->getResourceAmount()).upgradeValue; 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_GENERATOR1:
case Obj::CREATURE_GENERATOR2: case Obj::CREATURE_GENERATOR2:
case Obj::CREATURE_GENERATOR3: case Obj::CREATURE_GENERATOR3:
case Obj::CREATURE_GENERATOR4: case Obj::CREATURE_GENERATOR4:
return getDwellingArmyValue(ai->cb.get(), target, checkGold); return getDwellingArmyValue(ai->cb.get(), target, checkGold);
case Obj::CRYPT: case Obj::SPELL_SCROLL:
case Obj::SHIPWRECK: //FALL_THROUGH
case Obj::SHIPWRECK_SURVIVOR:
case Obj::WARRIORS_TOMB:
return 1000;
case Obj::ARTIFACT: case Obj::ARTIFACT:
return evaluateArtifactArmyValue(dynamic_cast<const CGArtifact *>(target)->storedArtifact); return evaluateArtifactArmyValue(dynamic_cast<const CGArtifact *>(target)->storedArtifact->getType());
case Obj::DRAGON_UTOPIA:
return 10000;
case Obj::HERO: case Obj::HERO:
return relations == PlayerRelations::ENEMIES return relations == PlayerRelations::ENEMIES
? enemyArmyEliminationRewardRatio * dynamic_cast<const CGHeroInstance *>(target)->getArmyStrength() ? enemyArmyEliminationRewardRatio * dynamic_cast<const CGHeroInstance *>(target)->getArmyStrength()
@ -328,8 +318,46 @@ uint64_t RewardEvaluator::getArmyReward(
case Obj::MAGIC_SPRING: case Obj::MAGIC_SPRING:
return getManaRecoveryArmyReward(hero); return getManaRecoveryArmyReward(hero);
default: 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( uint64_t RewardEvaluator::getArmyGrowth(
@ -468,12 +496,29 @@ uint64_t RewardEvaluator::townArmyGrowth(const CGTownInstance * town) const
return result; return result;
} }
uint64_t RewardEvaluator::getManaRecoveryArmyReward(const CGHeroInstance * hero) const float RewardEvaluator::getManaRecoveryArmyReward(const CGHeroInstance * hero) const
{ {
return ai->heroManager->getMagicStrength(hero) * 10000 * (1.0f - std::sqrt(static_cast<float>(hero->mana) / hero->manaLimit())); return ai->heroManager->getMagicStrength(hero) * 10000 * (1.0f - std::sqrt(static_cast<float>(hero->mana) / hero->manaLimit()));
} }
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) if(!target)
return 0; return 0;
@ -491,24 +536,10 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons
case Obj::RESOURCE: case Obj::RESOURCE:
{ {
auto resource = dynamic_cast<const CGResource *>(target); auto resource = dynamic_cast<const CGResource *>(target);
return resource->resourceID() == EGameResID::GOLD TResources res;
? 0 res[resource->resourceID()] = resource->amount;
: 0.2f * getTotalResourceRequirementStrength(resource->resourceID()) + 0.4f * getResourceRequirementStrength(resource->resourceID());
} return getResourceRequirementStrength(res);
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;
} }
case Obj::TOWN: case Obj::TOWN:
@ -546,6 +577,70 @@ float RewardEvaluator::getStrategicalValue(const CGObjectInstance * target) cons
case Obj::KEYMASTER: case Obj::KEYMASTER:
return 0.6f; 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: default:
return 0; return 0;
} }
@ -593,11 +688,11 @@ float RewardEvaluator::getSkillReward(const CGObjectInstance * target, const CGH
case Obj::ARENA: case Obj::ARENA:
return 2; return 2;
case Obj::SHRINE_OF_MAGIC_INCANTATION: case Obj::SHRINE_OF_MAGIC_INCANTATION:
return 0.2f; return 0.25f;
case Obj::SHRINE_OF_MAGIC_GESTURE: case Obj::SHRINE_OF_MAGIC_GESTURE:
return 0.3f; return 1.0f;
case Obj::SHRINE_OF_MAGIC_THOUGHT: case Obj::SHRINE_OF_MAGIC_THOUGHT:
return 0.5f; return 2.0f;
case Obj::LIBRARY_OF_ENLIGHTENMENT: case Obj::LIBRARY_OF_ENLIGHTENMENT:
return 8; return 8;
case Obj::WITCH_HUT: case Obj::WITCH_HUT:
@ -605,15 +700,55 @@ float RewardEvaluator::getSkillReward(const CGObjectInstance * target, const CGH
case Obj::PANDORAS_BOX: case Obj::PANDORAS_BOX:
//Can contains experience, spells, or skills (only on custom maps) //Can contains experience, spells, or skills (only on custom maps)
return 2.5f; return 2.5f;
case Obj::PYRAMID:
return 3.0f;
case Obj::HERO: case Obj::HERO:
return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES return ai->cb->getPlayerRelations(target->tempOwner, ai->playerID) == PlayerRelations::ENEMIES
? enemyHeroEliminationSkillRewardRatio * dynamic_cast<const CGHeroInstance *>(target)->level ? enemyHeroEliminationSkillRewardRatio * dynamic_cast<const CGHeroInstance *>(target)->level
: 0; : 0;
default: 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 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()) for(auto stack : army->Slots())
{ {
value += stack.second->getCreatureID().toCreature()->getRecruitCost(EGameResID::GOLD) * stack.second->count; value += stack.second->getCreatureID().toCreature()->getFullRecruitCost().marketValue() * stack.second->count;
} }
return value; return value;
@ -671,22 +806,6 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG
auto * mine = dynamic_cast<const CGMine*>(target); auto * mine = dynamic_cast<const CGMine*>(target);
return dailyIncomeMultiplier * (mine->producedResource == GameResID::GOLD ? 1000 : 75); 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: case Obj::PANDORAS_BOX:
return 2500; return 2500;
case Obj::PRISON: case Obj::PRISON:
@ -697,8 +816,26 @@ int32_t RewardEvaluator::getGoldReward(const CGObjectInstance * target, const CG
? heroEliminationBonus + enemyArmyEliminationGoldRewardRatio * getArmyCost(dynamic_cast<const CGHeroInstance *>(target)) ? heroEliminationBonus + enemyArmyEliminationGoldRewardRatio * getArmyCost(dynamic_cast<const CGHeroInstance *>(target))
: 0; : 0;
default: 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 class HeroExchangeEvaluator : public IEvaluationContextBuilder
@ -714,7 +851,9 @@ public:
uint64_t armyStrength = heroExchange.getReinforcementArmyStrength(evaluationContext.evaluator.ai); uint64_t armyStrength = heroExchange.getReinforcementArmyStrength(evaluationContext.evaluator.ai);
evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength()); evaluationContext.addNonCriticalStrategicalValue(2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength());
evaluationContext.conquestValue += 2.0f * armyStrength / (float)heroExchange.hero->getArmyStrength();
evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero); evaluationContext.heroRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(heroExchange.hero);
evaluationContext.isExchange = true;
} }
}; };
@ -732,6 +871,7 @@ public:
evaluationContext.armyReward += upgradeValue; evaluationContext.armyReward += upgradeValue;
evaluationContext.addNonCriticalStrategicalValue(upgradeValue / (float)armyUpgrade.hero->getArmyStrength()); evaluationContext.addNonCriticalStrategicalValue(upgradeValue / (float)armyUpgrade.hero->getArmyStrength());
evaluationContext.isArmyUpgrade = true;
} }
}; };
@ -746,22 +886,46 @@ public:
int tilesDiscovered = task->value; int tilesDiscovered = task->value;
evaluationContext.addNonCriticalStrategicalValue(0.03f * tilesDiscovered); evaluationContext.addNonCriticalStrategicalValue(0.03f * tilesDiscovered);
for (auto obj : evaluationContext.evaluator.ai->cb->getVisitableObjs(task->tile))
{
switch (obj->ID.num)
{
case Obj::MONOLITH_ONE_WAY_ENTRANCE:
case Obj::MONOLITH_TWO_WAY:
case Obj::SUBTERRANEAN_GATE:
evaluationContext.explorePriority = 1;
break;
case Obj::REDWOOD_OBSERVATORY:
case Obj::PILLAR_OF_FIRE:
evaluationContext.explorePriority = 2;
break;
}
}
if(evaluationContext.evaluator.ai->cb->getTile(task->tile)->roadType != RoadId::NO_ROAD)
evaluationContext.explorePriority = 1;
if (evaluationContext.explorePriority == 0)
evaluationContext.explorePriority = 3;
} }
}; };
class StayAtTownManaRecoveryEvaluator : public IEvaluationContextBuilder class StayAtTownManaRecoveryEvaluator : public IEvaluationContextBuilder
{ {
public: public:
void buildEvaluationContext(EvaluationContext & evaluationContext, Goals::TSubgoal task) const override void buildEvaluationContext(EvaluationContext& evaluationContext, Goals::TSubgoal task) const override
{ {
if(task->goalType != Goals::STAY_AT_TOWN) if (task->goalType != Goals::STAY_AT_TOWN)
return; return;
Goals::StayAtTown & stayAtTown = dynamic_cast<Goals::StayAtTown &>(*task); Goals::StayAtTown& stayAtTown = dynamic_cast<Goals::StayAtTown&>(*task);
evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero()); evaluationContext.armyReward += evaluationContext.evaluator.getManaRecoveryArmyReward(stayAtTown.getHero());
evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted(); if (evaluationContext.armyReward == 0)
evaluationContext.movementCost += stayAtTown.getMovementWasted(); evaluationContext.isDefend = true;
else
{
evaluationContext.movementCost += stayAtTown.getMovementWasted();
evaluationContext.movementCostByRole[evaluationContext.heroRole] += stayAtTown.getMovementWasted();
}
} }
}; };
@ -772,15 +936,8 @@ void addTileDanger(EvaluationContext & evaluationContext, const int3 & tile, uin
if(enemyDanger.danger) if(enemyDanger.danger)
{ {
auto dangerRatio = enemyDanger.danger / (double)ourStrength; auto dangerRatio = enemyDanger.danger / (double)ourStrength;
auto enemyHero = evaluationContext.evaluator.ai->cb->getObj(enemyDanger.hero.hid, false);
bool isAI = enemyHero && isAnotherAi(enemyHero, *evaluationContext.evaluator.ai->cb);
if(isAI)
{
dangerRatio *= 1.5; // lets make AI bit more afraid of other AI.
}
vstd::amax(evaluationContext.enemyHeroDangerRatio, dangerRatio); vstd::amax(evaluationContext.enemyHeroDangerRatio, dangerRatio);
vstd::amax(evaluationContext.threat, enemyDanger.threat);
} }
} }
@ -824,6 +981,10 @@ public:
else else
evaluationContext.addNonCriticalStrategicalValue(1.7f * multiplier * strategicalValue); evaluationContext.addNonCriticalStrategicalValue(1.7f * multiplier * strategicalValue);
evaluationContext.defenseValue = town->fortLevel();
evaluationContext.isDefend = true;
evaluationContext.threatTurns = treat.turn;
vstd::amax(evaluationContext.danger, defendTown.getTreat().danger); vstd::amax(evaluationContext.danger, defendTown.getTreat().danger);
addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength()); addTileDanger(evaluationContext, town->visitablePos(), defendTown.getTurn(), defendTown.getDefenceStrength());
} }
@ -845,6 +1006,9 @@ public:
Goals::ExecuteHeroChain & chain = dynamic_cast<Goals::ExecuteHeroChain &>(*task); Goals::ExecuteHeroChain & chain = dynamic_cast<Goals::ExecuteHeroChain &>(*task);
const AIPath & path = chain.getPath(); const AIPath & path = chain.getPath();
if (vstd::isAlmostZero(path.movementCost()))
return;
vstd::amax(evaluationContext.danger, path.getTotalDanger()); vstd::amax(evaluationContext.danger, path.getTotalDanger());
evaluationContext.movementCost += path.movementCost(); evaluationContext.movementCost += path.movementCost();
evaluationContext.closestWayRatio = chain.closestWayRatio; evaluationContext.closestWayRatio = chain.closestWayRatio;
@ -854,14 +1018,24 @@ public:
for(auto & node : path.nodes) for(auto & node : path.nodes)
{ {
vstd::amax(costsPerHero[node.targetHero], node.cost); vstd::amax(costsPerHero[node.targetHero], node.cost);
if (node.layer == EPathfindingLayer::SAIL)
evaluationContext.involvesSailing = true;
} }
float highestCostForSingleHero = 0;
for(auto pair : costsPerHero) for(auto pair : costsPerHero)
{ {
auto role = evaluationContext.evaluator.ai->heroManager->getHeroRole(pair.first); auto role = evaluationContext.evaluator.ai->heroManager->getHeroRole(pair.first);
evaluationContext.movementCostByRole[role] += pair.second; 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; auto hero = task->hero;
bool checkGold = evaluationContext.danger == 0; bool checkGold = evaluationContext.danger == 0;
@ -880,10 +1054,18 @@ public:
evaluationContext.armyGrowth += evaluationContext.evaluator.getArmyGrowth(target, hero, army); evaluationContext.armyGrowth += evaluationContext.evaluator.getArmyGrowth(target, hero, army);
evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, heroRole); evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, heroRole);
evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target)); evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target));
evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target);
if (target->ID == Obj::HERO)
evaluationContext.isHero = true;
if (target->getOwner().isValidPlayer() && ai->cb->getPlayerRelations(ai->playerID, target->getOwner()) == PlayerRelations::ENEMIES)
evaluationContext.isEnemy = true;
evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army); evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army);
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()); addTileDanger(evaluationContext, path.targetTile(), path.turn(), path.getHeroStrength());
vstd::amax(evaluationContext.turn, path.turn()); vstd::amax(evaluationContext.turn, path.turn());
} }
@ -924,6 +1106,7 @@ public:
evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold) / boost; evaluationContext.armyReward += evaluationContext.evaluator.getArmyReward(target, hero, army, checkGold) / boost;
evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, role) / boost; evaluationContext.skillReward += evaluationContext.evaluator.getSkillReward(target, hero, role) / boost;
evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target) / boost); evaluationContext.addNonCriticalStrategicalValue(evaluationContext.evaluator.getStrategicalValue(target) / boost);
evaluationContext.conquestValue += evaluationContext.evaluator.getConquestValue(target);
evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army) / boost; evaluationContext.goldCost += evaluationContext.evaluator.getGoldCost(target, hero, army) / boost;
evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost; evaluationContext.movementCostByRole[role] += objInfo.second.movementCost / boost;
evaluationContext.movementCost += objInfo.second.movementCost / boost; evaluationContext.movementCost += objInfo.second.movementCost / boost;
@ -949,6 +1132,14 @@ public:
Goals::ExchangeSwapTownHeroes & swapCommand = dynamic_cast<Goals::ExchangeSwapTownHeroes &>(*task); Goals::ExchangeSwapTownHeroes & swapCommand = dynamic_cast<Goals::ExchangeSwapTownHeroes &>(*task);
const CGHeroInstance * garrisonHero = swapCommand.getGarrisonHero(); const CGHeroInstance * garrisonHero = swapCommand.getGarrisonHero();
logAi->trace("buildEvaluationContext ExchangeSwapTownHeroesContextBuilder %s affected objects: %d", swapCommand.toString(), swapCommand.getAffectedObjects().size());
for (auto obj : swapCommand.getAffectedObjects())
{
logAi->trace("affected object: %s", evaluationContext.evaluator.ai->cb->getObj(obj)->getObjectName());
}
if (garrisonHero)
logAi->debug("with %s and %d", garrisonHero->getNameTranslated(), int(swapCommand.getLockingReason()));
if(garrisonHero && swapCommand.getLockingReason() == HeroLockedReason::DEFENCE) if(garrisonHero && swapCommand.getLockingReason() == HeroLockedReason::DEFENCE)
{ {
auto defenderRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(garrisonHero); auto defenderRole = evaluationContext.evaluator.ai->heroManager->getHeroRole(garrisonHero);
@ -957,6 +1148,9 @@ public:
evaluationContext.movementCost += mpLeft; evaluationContext.movementCost += mpLeft;
evaluationContext.movementCostByRole[defenderRole] += mpLeft; evaluationContext.movementCostByRole[defenderRole] += mpLeft;
evaluationContext.heroRole = defenderRole; evaluationContext.heroRole = defenderRole;
evaluationContext.isDefend = true;
evaluationContext.armyInvolvement = garrisonHero->getArmyStrength();
logAi->debug("evaluationContext.isDefend: %d", evaluationContext.isDefend);
} }
} }
}; };
@ -1000,8 +1194,14 @@ public:
evaluationContext.goldReward += 7 * bi.dailyIncome[EGameResID::GOLD] / 2; // 7 day income but half we already have evaluationContext.goldReward += 7 * bi.dailyIncome[EGameResID::GOLD] / 2; // 7 day income but half we already have
evaluationContext.heroRole = HeroRole::MAIN; evaluationContext.heroRole = HeroRole::MAIN;
evaluationContext.movementCostByRole[evaluationContext.heroRole] += bi.prerequisitesCount; evaluationContext.movementCostByRole[evaluationContext.heroRole] += bi.prerequisitesCount;
evaluationContext.goldCost += bi.buildCostWithPrerequisits[EGameResID::GOLD]; int32_t cost = bi.buildCost[EGameResID::GOLD];
evaluationContext.goldCost += cost;
evaluationContext.closestWayRatio = 1; evaluationContext.closestWayRatio = 1;
evaluationContext.buildingCost += bi.buildCostWithPrerequisites;
if (bi.id == BuildingID::MARKETPLACE || bi.dailyIncome[EGameResID::WOOD] > 0)
evaluationContext.isTradeBuilding = true;
logAi->trace("Building costs for %s : %s MarketValue: %d",bi.toString(), evaluationContext.buildingCost.toString(), evaluationContext.buildingCost.marketValue());
if(bi.creatureID != CreatureID::NONE) if(bi.creatureID != CreatureID::NONE)
{ {
@ -1028,7 +1228,18 @@ public:
else if(bi.id >= BuildingID::MAGES_GUILD_1 && bi.id <= BuildingID::MAGES_GUILD_5) else if(bi.id >= BuildingID::MAGES_GUILD_1 && bi.id <= BuildingID::MAGES_GUILD_5)
{ {
evaluationContext.skillReward += 2 * (bi.id - BuildingID::MAGES_GUILD_1); evaluationContext.skillReward += 2 * (bi.id - BuildingID::MAGES_GUILD_1);
for (auto hero : evaluationContext.evaluator.ai->cb->getHeroesInfo())
{
evaluationContext.armyInvolvement += hero->getArmyCost();
}
} }
int sameTownBonus = 0;
for (auto town : evaluationContext.evaluator.ai->cb->getTownsInfo())
{
if (buildThis.town->getFaction() == town->getFaction())
sameTownBonus += town->getTownLevel();
}
evaluationContext.armyReward *= sameTownBonus;
if(evaluationContext.goldReward) if(evaluationContext.goldReward)
{ {
@ -1048,7 +1259,7 @@ public:
uint64_t RewardEvaluator::getUpgradeArmyReward(const CGTownInstance * town, const BuildingInfo & bi) const 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; return 0;
auto creaturesToUpgrade = ai->armyManager->getTotalCreaturesAvailable(bi.baseCreatureID); auto creaturesToUpgrade = ai->armyManager->getTotalCreaturesAvailable(bi.baseCreatureID);
@ -1090,6 +1301,7 @@ EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal
for(auto subgoal : parts) for(auto subgoal : parts)
{ {
context.goldCost += subgoal->goldCost; context.goldCost += subgoal->goldCost;
context.buildingCost += subgoal->buildingCost;
for(auto builder : evaluationContextBuilders) for(auto builder : evaluationContextBuilders)
{ {
@ -1100,7 +1312,7 @@ EvaluationContext PriorityEvaluator::buildEvaluationContext(Goals::TSubgoal goal
return context; return context;
} }
float PriorityEvaluator::evaluate(Goals::TSubgoal task) float PriorityEvaluator::evaluate(Goals::TSubgoal task, int priorityTier)
{ {
auto evaluationContext = buildEvaluationContext(task); auto evaluationContext = buildEvaluationContext(task);
@ -1113,47 +1325,284 @@ float PriorityEvaluator::evaluate(Goals::TSubgoal task)
double result = 0; double result = 0;
try if (ai->settings->isUseFuzzy())
{ {
armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage); float fuzzyResult = 0;
heroRoleVariable->setValue(evaluationContext.heroRole); try
mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]); {
scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]); armyLossPersentageVariable->setValue(evaluationContext.armyLossPersentage);
goldRewardVariable->setValue(goldRewardPerTurn); heroRoleVariable->setValue(evaluationContext.heroRole);
armyRewardVariable->setValue(evaluationContext.armyReward); mainTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::MAIN]);
armyGrowthVariable->setValue(evaluationContext.armyGrowth); scoutTurnDistanceVariable->setValue(evaluationContext.movementCostByRole[HeroRole::SCOUT]);
skillRewardVariable->setValue(evaluationContext.skillReward); goldRewardVariable->setValue(goldRewardPerTurn);
dangerVariable->setValue(evaluationContext.danger); armyRewardVariable->setValue(evaluationContext.armyReward);
rewardTypeVariable->setValue(rewardType); armyGrowthVariable->setValue(evaluationContext.armyGrowth);
closestHeroRatioVariable->setValue(evaluationContext.closestWayRatio); skillRewardVariable->setValue(evaluationContext.skillReward);
strategicalValueVariable->setValue(evaluationContext.strategicalValue); dangerVariable->setValue(evaluationContext.danger);
goldPressureVariable->setValue(ai->buildAnalyzer->getGoldPressure()); rewardTypeVariable->setValue(rewardType);
goldCostVariable->setValue(evaluationContext.goldCost / ((float)ai->getFreeResources()[EGameResID::GOLD] + (float)ai->buildAnalyzer->getDailyIncome()[EGameResID::GOLD] + 1.0f)); closestHeroRatioVariable->setValue(evaluationContext.closestWayRatio);
turnVariable->setValue(evaluationContext.turn); strategicalValueVariable->setValue(evaluationContext.strategicalValue);
fearVariable->setValue(evaluationContext.enemyHeroDangerRatio); goldPressureVariable->setValue(ai->buildAnalyzer->getGoldPressure());
goldCostVariable->setValue(evaluationContext.goldCost / ((float)ai->getFreeResources()[EGameResID::GOLD] + (float)ai->buildAnalyzer->getDailyIncome()[EGameResID::GOLD] + 1.0f));
turnVariable->setValue(evaluationContext.turn);
fearVariable->setValue(evaluationContext.enemyHeroDangerRatio);
engine->process(); engine->process();
result = value->getValue(); fuzzyResult = value->getValue();
}
catch (fl::Exception& fe)
{
logAi->error("evaluate VisitTile: %s", fe.getWhat());
}
result = fuzzyResult;
} }
catch(fl::Exception & fe) else
{ {
logAi->error("evaluate VisitTile: %s", fe.getWhat()); float score = 0;
const bool amIInDanger = ai->cb->getTownsInfo().empty() || (evaluationContext.isDefend && evaluationContext.threatTurns == 0);
const float maxWillingToLose = amIInDanger ? 1 : ai->settings->getMaxArmyLossTarget();
bool arriveNextWeek = false;
if (ai->cb->getDate(Date::DAY_OF_WEEK) + evaluationContext.turn > 7 && 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 #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(), task->toString(),
evaluationContext.armyLossPersentage, evaluationContext.armyLossPersentage,
(int)evaluationContext.turn, (int)evaluationContext.turn,
evaluationContext.movementCostByRole[HeroRole::MAIN], evaluationContext.movementCostByRole[HeroRole::MAIN],
evaluationContext.movementCostByRole[HeroRole::SCOUT], evaluationContext.movementCostByRole[HeroRole::SCOUT],
evaluationContext.armyInvolvement,
goldRewardPerTurn, goldRewardPerTurn,
evaluationContext.goldCost, evaluationContext.goldCost,
evaluationContext.armyReward, evaluationContext.armyReward,
evaluationContext.armyGrowth,
evaluationContext.skillReward,
evaluationContext.danger, evaluationContext.danger,
evaluationContext.threatTurns,
evaluationContext.threat,
evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout", evaluationContext.heroRole == HeroRole::MAIN ? "main" : "scout",
evaluationContext.strategicalValue, evaluationContext.strategicalValue,
evaluationContext.conquestValue,
evaluationContext.closestWayRatio, evaluationContext.closestWayRatio,
evaluationContext.enemyHeroDangerRatio, evaluationContext.enemyHeroDangerRatio,
result); result);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -34,13 +34,13 @@ void BuyArmy::accept(AIGateway * ai)
ui64 valueBought = 0; ui64 valueBought = 0;
//buy the stacks with largest AI value //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); auto armyToBuy = ai->nullkiller->armyManager->getArmyAvailableToBuy(town->getUpperArmy(), town);
if(armyToBuy.empty()) if(armyToBuy.empty())
{ {
if(upgradeSuccessfull) if(upgradeSuccessful)
return; return;
throw cannotFulfillGoalException("No creatures to buy."); throw cannotFulfillGoalException("No creatures to buy.");
@ -58,7 +58,36 @@ void BuyArmy::accept(AIGateway * ai)
if(ci.count) 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(); 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 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 bool operator==(const AbstractGoal & g) const override
{ {

View File

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

View File

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

View File

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

View File

@ -22,11 +22,17 @@ ExecuteHeroChain::ExecuteHeroChain(const AIPath & path, const CGObjectInstance *
{ {
hero = path.targetHero; hero = path.targetHero;
tile = path.targetTile(); tile = path.targetTile();
closestWayRatio = 1;
if(obj) if(obj)
{ {
objid = obj->id.getNum(); 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 else
{ {
@ -80,6 +86,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
ai->nullkiller->setActive(chainPath.targetHero, tile); ai->nullkiller->setActive(chainPath.targetHero, tile);
ai->nullkiller->setTargetObject(objid); ai->nullkiller->setTargetObject(objid);
ai->nullkiller->objectClusterizer->reset();
auto targetObject = ai->myCb->getObj(static_cast<ObjectInstanceID>(objid), false); auto targetObject = ai->myCb->getObj(static_cast<ObjectInstanceID>(objid), false);
@ -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()) if(hero->movementPointsRemaining())
{ {
try try
@ -234,7 +261,7 @@ void ExecuteHeroChain::accept(AIGateway * ai)
if(node->turns == 0) if(node->turns == 0)
{ {
logAi->error( 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(), hero->getNameTranslated(),
node->coord.toString(), node->coord.toString(),
hero->visitablePos().toString()); hero->visitablePos().toString());
@ -260,7 +287,11 @@ void ExecuteHeroChain::accept(AIGateway * ai)
std::string ExecuteHeroChain::toString() const std::string ExecuteHeroChain::toString() const
{ {
#if NKAI_TRACE_LEVEL >= 1
return "ExecuteHeroChain " + targetName + " by " + chainPath.toString();
#else
return "ExecuteHeroChain " + targetName + " by " + chainPath.targetHero->getNameTranslated(); return "ExecuteHeroChain " + targetName + " by " + chainPath.targetHero->getNameTranslated();
#endif
} }
bool ExecuteHeroChain::moveHeroToTile(AIGateway * ai, const CGHeroInstance * hero, const int3 & tile) bool ExecuteHeroChain::moveHeroToTile(AIGateway * ai, const CGHeroInstance * hero, const int3 & tile)

View File

@ -59,7 +59,7 @@ void ExploreNeighbourTile::accept(AIGateway * ai)
return; 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)) 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!"); throw cannotFulfillGoalException("Town " + t->nodeName() + " is occupied. Cannot recruit hero!");
cb->recruitHero(t, heroToHire); 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; std::string toString() const override;
const CGHeroInstance* getHero() const override { return heroToBuy; }
void accept(AIGateway * ai) override; void accept(AIGateway * ai) override;
}; };
} }

View File

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

View File

@ -14,27 +14,37 @@
namespace NKAI 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()) 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 return slot.second->getCount() == 1
? std::numeric_limits<int>::max() ? std::numeric_limits<int>::max()
: slot.second->getCreatureID().toCreature()->getAIValue(); : slot.second->getCreatureID().toCreature()->getAIValue();
}); });
if(weakestCreature == attacker->Slots().end() || weakestCreature->second->getCount() == 1) if(weakestCreature == hero->Slots().end() || weakestCreature->second->getCount() == 1)
{ {
break; break;
} }
cb->splitStack(attacker, attacker, weakestCreature->first, freeSlots.front(), 1); cb->splitStack(hero, hero, weakestCreature->first, freeSlots.front(), 1);
freeSlots.pop(); freeSlots.pop();
} }
}
void ArmyFormation::rearrangeArmyForSiege(const CGTownInstance * town, const CGHeroInstance * attacker)
{
addSingleCreatureStacks(attacker);
if(town->fortLevel() > CGTownInstance::FORT) if(town->fortLevel() > CGTownInstance::FORT)
{ {

View File

@ -13,8 +13,6 @@
#include "../../../lib/GameConstants.h" #include "../../../lib/GameConstants.h"
#include "../../../lib/VCMI_Lib.h" #include "../../../lib/VCMI_Lib.h"
#include "../../../lib/CTownHandler.h"
#include "../../../lib/CBuildingHandler.h"
namespace NKAI namespace NKAI
{ {
@ -33,6 +31,10 @@ public:
ArmyFormation(std::shared_ptr<CCallback> CB, const Nullkiller * ai): cb(CB) {} ArmyFormation(std::shared_ptr<CCallback> CB, const Nullkiller * ai): cb(CB) {}
void rearrangeArmyForSiege(const CGTownInstance * town, const CGHeroInstance * attacker); 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); 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++) 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) 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; bool hasInvisibleNeighbor = false;
foreach_neighbour(cbp, pos, [&](CCallback * cbp, int3 neighbour) 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; hasInvisibleNeighbor = true;
} }
@ -107,7 +107,7 @@ bool ExplorationHelper::scanMap()
allowDeadEndCancellation = false; allowDeadEndCancellation = false;
logAi->debug("Exploration scan all possible tiles for hero %s", hero->getNameTranslated()); 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; std::vector<int3> tilesToExploreFrom = edgeTiles;
// WARNING: POTENTIAL BUG // WARNING: POTENTIAL BUG
@ -175,7 +175,7 @@ void ExplorationHelper::scanTile(const int3 & tile)
continue; continue;
} }
if(isSafeToVisit(hero, path.heroArmy, path.getTotalDanger())) if(isSafeToVisit(hero, path.heroArmy, path.getTotalDanger(), ai->settings->getSafeAttackRatio()))
{ {
bestGoal = goal; bestGoal = goal;
bestValue = ourValue; bestValue = ourValue;
@ -191,7 +191,7 @@ int ExplorationHelper::howManyTilesWillBeDiscovered(const int3 & pos) const
int ret = 0; int ret = 0;
int3 npos = int3(0, 0, pos.z); 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++) 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/GameConstants.h"
#include "../../../lib/VCMI_Lib.h" #include "../../../lib/VCMI_Lib.h"
#include "../../../lib/CTownHandler.h"
#include "../../../lib/CBuildingHandler.h"
#include "../Goals/AbstractGoal.h" #include "../Goals/AbstractGoal.h"
namespace NKAI namespace NKAI

View File

@ -10,6 +10,7 @@
#include "StdInc.h" #include "StdInc.h"
#include "AINodeStorage.h" #include "AINodeStorage.h"
#include "Actions/TownPortalAction.h" #include "Actions/TownPortalAction.h"
#include "Actions/WhirlpoolAction.h"
#include "../Goals/Goals.h" #include "../Goals/Goals.h"
#include "../AIGateway.h" #include "../AIGateway.h"
#include "../Engine/Nullkiller.h" #include "../Engine/Nullkiller.h"
@ -25,10 +26,10 @@ namespace NKAI
{ {
std::shared_ptr<boost::multi_array<AIPathNode, 4>> AISharedStorage::shared; std::shared_ptr<boost::multi_array<AIPathNode, 4>> AISharedStorage::shared;
uint64_t AISharedStorage::version = 0; uint32_t AISharedStorage::version = 0;
boost::mutex AISharedStorage::locker; boost::mutex AISharedStorage::locker;
std::set<int3> commitedTiles; std::set<int3> committedTiles;
std::set<int3> commitedTilesInitial; std::set<int3> committedTilesInitial;
const uint64_t FirstActorMask = 1; 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 MIN_ARMY_STRENGTH_FOR_NEXT_ACTOR = 1000;
const uint64_t CHAIN_MAX_DEPTH = 4; 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){ if(!shared){
shared.reset(new boost::multi_array<AIPathNode, 4>( shared.reset(new boost::multi_array<AIPathNode, 4>(
boost::extents[sizes.z][sizes.x][sizes.y][AIPathfinding::NUM_CHAINS])); boost::extents[sizes.z][sizes.x][sizes.y][numChains]));
nodes = shared; nodes = shared;
foreach_tile_pos([&](const int3 & pos) foreach_tile_pos([&](const int3 & pos)
{ {
for(auto i = 0; i < AIPathfinding::NUM_CHAINS; i++) for(auto i = 0; i < numChains; i++)
{ {
auto & node = get(pos)[i]; auto & node = get(pos)[i];
@ -91,13 +92,21 @@ void AIPathNode::addSpecialAction(std::shared_ptr<const SpecialAction> action)
} }
} }
AINodeStorage::AINodeStorage(const Nullkiller * ai, const int3 & Sizes) int AINodeStorage::getBucketCount() const
: sizes(Sizes), ai(ai), cb(ai->cb.get()), nodes(Sizes)
{ {
accesibility = std::make_unique<boost::multi_array<EPathAccessibility, 4>>( return ai->settings->getPathfinderBucketsCount();
boost::extents[sizes.z][sizes.x][sizes.y][EPathfindingLayer::NUM_LAYERS]); }
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; 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) for(pos.y = 0; pos.y < sizes.y; ++pos.y)
{ {
const TerrainTile & tile = gs->map->getTile(pos); const TerrainTile & tile = gs->map->getTile(pos);
if (!tile.terType->isPassable()) if (!tile.getTerrain()->isPassable())
continue; continue;
if (tile.terType->isWater()) if (tile.isWater())
{ {
resetTile(pos, ELayer::SAIL, PathfinderUtil::evaluateAccessibility<ELayer::SAIL>(pos, tile, fow, player, gs)); resetTile(pos, ELayer::SAIL, PathfinderUtil::evaluateAccessibility<ELayer::SAIL>(pos, tile, fow, player, gs));
if (useFlying) if (useFlying)
@ -157,7 +166,7 @@ void AINodeStorage::initialize(const PathfinderOptions & options, const CGameSta
void AINodeStorage::clear() void AINodeStorage::clear()
{ {
actors.clear(); actors.clear();
commitedTiles.clear(); committedTiles.clear();
heroChainPass = EHeroChainPass::INITIAL; heroChainPass = EHeroChainPass::INITIAL;
heroChainTurn = 0; heroChainTurn = 0;
heroChainMaxTurns = 1; heroChainMaxTurns = 1;
@ -170,8 +179,8 @@ std::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
const EPathfindingLayer layer, const EPathfindingLayer layer,
const ChainActor * actor) const ChainActor * actor)
{ {
int bucketIndex = ((uintptr_t)actor + static_cast<uint32_t>(layer)) % AIPathfinding::BUCKET_COUNT; int bucketIndex = ((uintptr_t)actor + static_cast<uint32_t>(layer)) % ai->settings->getPathfinderBucketsCount();
int bucketOffset = bucketIndex * AIPathfinding::BUCKET_SIZE; int bucketOffset = bucketIndex * ai->settings->getPathfinderBucketSize();
auto chains = nodes.get(pos); auto chains = nodes.get(pos);
if(blocked(pos, layer)) if(blocked(pos, layer))
@ -179,7 +188,7 @@ std::optional<AIPathNode *> AINodeStorage::getOrCreateNode(
return std::nullopt; return std::nullopt;
} }
for(auto i = AIPathfinding::BUCKET_SIZE - 1; i >= 0; i--) for(auto i = ai->settings->getPathfinderBucketSize() - 1; i >= 0; i--)
{ {
AIPathNode & node = chains[i + bucketOffset]; AIPathNode & node = chains[i + bucketOffset];
@ -224,7 +233,6 @@ std::vector<CGPathNode *> AINodeStorage::getInitialNodes()
AIPathNode * initialNode = allocated.value(); AIPathNode * initialNode = allocated.value();
initialNode->inPQ = false;
initialNode->pq = nullptr; initialNode->pq = nullptr;
initialNode->turns = actor->initialTurn; initialNode->turns = actor->initialTurn;
initialNode->moveRemains = actor->initialMovement; 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); 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 // 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) if(dstNode->specialAction && dstNode->actor)
@ -276,7 +319,7 @@ void AINodeStorage::commit(
int turn, int turn,
int movementLeft, int movementLeft,
float cost, float cost,
bool saveToCommited) const bool saveToCommitted) const
{ {
destination->action = action; destination->action = action;
destination->setCost(cost); destination->setCost(cost);
@ -290,7 +333,7 @@ void AINodeStorage::commit(
#if NKAI_PATHFINDER_TRACE_LEVEL >= 2 #if NKAI_PATHFINDER_TRACE_LEVEL >= 2
logAi->trace( 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(), source->coord.toString(),
destination->coord.toString(), destination->coord.toString(),
destination->layer, destination->layer,
@ -302,9 +345,9 @@ void AINodeStorage::commit(
destination->actor->armyValue); destination->actor->armyValue);
#endif #endif
if(saveToCommited && destination->turns <= heroChainTurn) if(saveToCommitted && destination->turns <= heroChainTurn)
{ {
commitedTiles.insert(destination->coord); committedTiles.insert(destination->coord);
} }
if(destination->turns == source->turns) if(destination->turns == source->turns)
@ -374,7 +417,7 @@ bool AINodeStorage::increaseHeroChainTurnLimit()
return false; return false;
heroChainTurn++; heroChainTurn++;
commitedTiles.clear(); committedTiles.clear();
for(auto layer : phisycalLayers) for(auto layer : phisycalLayers)
{ {
@ -384,7 +427,7 @@ bool AINodeStorage::increaseHeroChainTurnLimit()
{ {
if(node.turns <= heroChainTurn && node.action != EPathNodeAction::UNKNOWN) if(node.turns <= heroChainTurn && node.action != EPathNodeAction::UNKNOWN)
{ {
commitedTiles.insert(pos); committedTiles.insert(pos);
return true; return true;
} }
@ -453,8 +496,8 @@ public:
AINodeStorage & storage, const std::vector<int3> & tiles, uint64_t chainMask, int heroChainTurn) AINodeStorage & storage, const std::vector<int3> & tiles, uint64_t chainMask, int heroChainTurn)
:existingChains(), newChains(), delayedWork(), storage(storage), chainMask(chainMask), heroChainTurn(heroChainTurn), heroChain(), tiles(tiles) :existingChains(), newChains(), delayedWork(), storage(storage), chainMask(chainMask), heroChainTurn(heroChainTurn), heroChain(), tiles(tiles)
{ {
existingChains.reserve(AIPathfinding::NUM_CHAINS); existingChains.reserve(storage.getBucketCount() * storage.getBucketSize());
newChains.reserve(AIPathfinding::NUM_CHAINS); newChains.reserve(storage.getBucketCount() * storage.getBucketSize());
} }
void execute(const tbb::blocked_range<size_t>& r) void execute(const tbb::blocked_range<size_t>& r)
@ -545,7 +588,7 @@ bool AINodeStorage::calculateHeroChain()
heroChainPass = EHeroChainPass::CHAIN; heroChainPass = EHeroChainPass::CHAIN;
heroChain.clear(); heroChain.clear();
std::vector<int3> data(commitedTiles.begin(), commitedTiles.end()); std::vector<int3> data(committedTiles.begin(), committedTiles.end());
if(data.size() > 100) if(data.size() > 100)
{ {
@ -576,7 +619,7 @@ bool AINodeStorage::calculateHeroChain()
task.flushResult(heroChain); task.flushResult(heroChain);
} }
commitedTiles.clear(); committedTiles.clear();
return !heroChain.empty(); return !heroChain.empty();
} }
@ -592,7 +635,7 @@ bool AINodeStorage::selectFirstActor()
}); });
chainMask = strongest->chainMask; chainMask = strongest->chainMask;
commitedTilesInitial = commitedTiles; committedTilesInitial = committedTiles;
return true; return true;
} }
@ -627,7 +670,7 @@ bool AINodeStorage::selectNextActor()
return false; return false;
chainMask = nextActor->get()->chainMask; chainMask = nextActor->get()->chainMask;
commitedTiles = commitedTilesInitial; committedTiles = committedTilesInitial;
return true; return true;
} }
@ -654,7 +697,7 @@ void HeroChainCalculationTask::cleanupInefectiveChains(std::vector<ExchangeCandi
if(isNotEffective) if(isNotEffective)
{ {
logAi->trace( 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->toString(),
chainInfo.otherParent->actor->chainMask, chainInfo.otherParent->actor->chainMask,
chainInfo.carrierParent->actor->toString(), chainInfo.carrierParent->actor->toString(),
@ -686,6 +729,7 @@ void HeroChainCalculationTask::calculateHeroChain(
if(node->action == EPathNodeAction::BATTLE if(node->action == EPathNodeAction::BATTLE
|| node->action == EPathNodeAction::TELEPORT_BATTLE || node->action == EPathNodeAction::TELEPORT_BATTLE
|| node->action == EPathNodeAction::TELEPORT_NORMAL || node->action == EPathNodeAction::TELEPORT_NORMAL
|| node->action == EPathNodeAction::DISEMBARK
|| node->action == EPathNodeAction::TELEPORT_BLOCKING_VISIT) || node->action == EPathNodeAction::TELEPORT_BLOCKING_VISIT)
{ {
continue; continue;
@ -754,7 +798,7 @@ void HeroChainCalculationTask::calculateHeroChain(
if(hasLessMp && hasLessExperience) if(hasLessMp && hasLessExperience)
{ {
#if NKAI_PATHFINDER_TRACE_LEVEL >= 2 #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 #endif
return; return;
} }
@ -823,7 +867,7 @@ void HeroChainCalculationTask::addHeroChain(const std::vector<ExchangeCandidate>
chainInfo.turns, chainInfo.turns,
chainInfo.moveRemains, chainInfo.moveRemains,
chainInfo.getCost(), chainInfo.getCost(),
DO_NOT_SAVE_TO_COMMITED_TILES); DO_NOT_SAVE_TO_COMMITTED_TILES);
if(carrier->specialAction || carrier->chainOther) 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 // do not allow our own heroes in garrison to act on map
if(hero.first->getOwner() == ai->playerID if(hero.first->getOwner() == ai->playerID
&& hero.first->inTownGarrison && hero.first->inTownGarrison
&& (ai->isHeroLocked(hero.first) || ai->heroManager->heroCapReached())) && (ai->isHeroLocked(hero.first) || ai->heroManager->heroCapReached(false)))
{ {
continue; continue;
} }
@ -1015,8 +1059,8 @@ std::vector<CGPathNode *> AINodeStorage::calculateTeleportations(
for(auto & neighbour : accessibleExits) 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) if(!node)
continue; continue;
@ -1027,7 +1071,7 @@ std::vector<CGPathNode *> AINodeStorage::calculateTeleportations(
return neighbours; return neighbours;
} }
struct TowmPortalFinder struct TownPortalFinder
{ {
const std::vector<CGPathNode *> & initialNodes; const std::vector<CGPathNode *> & initialNodes;
MasteryLevel::Type townPortalSkillLevel; MasteryLevel::Type townPortalSkillLevel;
@ -1040,7 +1084,7 @@ struct TowmPortalFinder
SpellID spellID; SpellID spellID;
const CSpell * townPortal; const CSpell * townPortal;
TowmPortalFinder( TownPortalFinder(
const ChainActor * actor, const ChainActor * actor,
const std::vector<CGPathNode *> & initialNodes, const std::vector<CGPathNode *> & initialNodes,
std::vector<const CGTownInstance *> targetTowns, std::vector<const CGTownInstance *> targetTowns,
@ -1117,7 +1161,7 @@ struct TowmPortalFinder
bestNode->turns, bestNode->turns,
bestNode->moveRemains - movementNeeded, bestNode->moveRemains - movementNeeded,
movementCost, movementCost,
DO_NOT_SAVE_TO_COMMITED_TILES); DO_NOT_SAVE_TO_COMMITTED_TILES);
node->theNodeBefore = bestNode; node->theNodeBefore = bestNode;
node->addSpecialAction(std::make_shared<AIPathfinding::TownPortalAction>(targetTown)); node->addSpecialAction(std::make_shared<AIPathfinding::TownPortalAction>(targetTown));
@ -1146,7 +1190,7 @@ void AINodeStorage::calculateTownPortal(
return; // no towns no need to run loop further return; // no towns no need to run loop further
} }
TowmPortalFinder townPortalFinder(actor, initialNodes, towns, this); TownPortalFinder townPortalFinder(actor, initialNodes, towns, this);
if(townPortalFinder.actorCanCastTownPortal()) if(townPortalFinder.actorCanCastTownPortal())
{ {
@ -1163,6 +1207,11 @@ void AINodeStorage::calculateTownPortal(
continue; continue;
} }
if (targetTown->visitingHero
&& (targetTown->visitingHero.get()->getFactionID() != actor->hero->getFactionID()
|| targetTown->getUpperArmy()->stacksCount()))
continue;
auto nodeOptional = townPortalFinder.createTownPortalNode(targetTown); auto nodeOptional = townPortalFinder.createTownPortalNode(targetTown);
if(nodeOptional) if(nodeOptional)
@ -1279,7 +1328,7 @@ bool AINodeStorage::isOtherChainBetter(
{ {
#if NKAI_PATHFINDER_TRACE_LEVEL >= 2 #if NKAI_PATHFINDER_TRACE_LEVEL >= 2
logAi->trace( 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(), source->coord.toString(),
candidateNode.coord.toString(), candidateNode.coord.toString(),
candidateNode.actor->hero->getNameTranslated(), candidateNode.actor->hero->getNameTranslated(),
@ -1303,7 +1352,7 @@ bool AINodeStorage::isOtherChainBetter(
{ {
#if NKAI_PATHFINDER_TRACE_LEVEL >= 2 #if NKAI_PATHFINDER_TRACE_LEVEL >= 2
logAi->trace( 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(), source->coord.toString(),
candidateNode.coord.toString(), candidateNode.coord.toString(),
candidateNode.actor->hero->getNameTranslated(), candidateNode.actor->hero->getNameTranslated(),
@ -1329,7 +1378,7 @@ bool AINodeStorage::isOtherChainBetter(
#if NKAI_PATHFINDER_TRACE_LEVEL >= 2 #if NKAI_PATHFINDER_TRACE_LEVEL >= 2
logAi->trace( 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(), source->coord.toString(),
candidateNode.coord.toString(), candidateNode.coord.toString(),
candidateNode.actor->hero->getNameTranslated(), candidateNode.actor->hero->getNameTranslated(),
@ -1384,7 +1433,33 @@ void AINodeStorage::calculateChainInfo(std::vector<AIPath> & paths, const int3 &
path.targetHero = node.actor->hero; path.targetHero = node.actor->hero;
path.heroArmy = node.actor->creatureSet; path.heroArmy = node.actor->creatureSet;
path.armyLoss = node.armyLoss; 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.targetObjectArmyLoss = evaluateArmyLoss(
path.targetHero, path.targetHero,
@ -1509,7 +1584,7 @@ uint8_t AIPath::turn() const
uint64_t AIPath::getHeroStrength() const uint64_t AIPath::getHeroStrength() const
{ {
return targetHero->getFightingStrength() * getHeroArmyStrengthWithCommander(targetHero, heroArmy); return targetHero->getHeroStrength() * getHeroArmyStrengthWithCommander(targetHero, heroArmy);
} }
uint64_t AIPath::getTotalDanger() const uint64_t AIPath::getTotalDanger() const

View File

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

View File

@ -13,7 +13,7 @@
#include "Rules/AIMovementAfterDestinationRule.h" #include "Rules/AIMovementAfterDestinationRule.h"
#include "Rules/AIMovementToDestinationRule.h" #include "Rules/AIMovementToDestinationRule.h"
#include "Rules/AIPreviousNodeRule.h" #include "Rules/AIPreviousNodeRule.h"
#include "../Engine//Nullkiller.h" #include "../Engine/Nullkiller.h"
#include "../../../lib/pathfinder/CPathfinder.h" #include "../../../lib/pathfinder/CPathfinder.h"
@ -44,10 +44,12 @@ namespace AIPathfinding
Nullkiller * ai, Nullkiller * ai,
std::shared_ptr<AINodeStorage> nodeStorage, std::shared_ptr<AINodeStorage> nodeStorage,
bool allowBypassObjects) 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.canUseCast = true;
options.allowLayerTransitioningAfterBattle = true; options.allowLayerTransitioningAfterBattle = true;
options.useTeleportWhirlpool = true;
options.forceUseTeleportWhirlpool = true;
} }
AIPathfinderConfig::~AIPathfinderConfig() = default; 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(); initialMovement = hero->movementPointsRemaining();
initialTurn = 0; initialTurn = 0;
armyValue = getHeroArmyStrengthWithCommander(hero, hero); armyValue = getHeroArmyStrengthWithCommander(hero, hero);
heroFightingStrength = hero->getFightingStrength(); heroFightingStrength = hero->getHeroStrength();
tiCache.reset(new TurnInfo(hero)); tiCache.reset(new TurnInfo(hero));
} }
@ -182,7 +182,7 @@ ExchangeResult HeroActor::tryExchangeNoLock(const ChainActor * specialActor, con
return &actor == specialActor; return &actor == specialActor;
}); });
result.actor = &(dynamic_cast<HeroActor *>(result.actor)->specialActors[index]); result.actor = &(dynamic_cast<HeroActor *>(result.actor)->specialActors.at(index));
return result; return result;
} }
@ -217,7 +217,7 @@ ExchangeResult HeroExchangeMap::tryExchangeNoLock(const ChainActor * other)
ExchangeResult result; 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()) 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()) if(!uniqueLock.owns_lock())
{ {
@ -374,10 +374,12 @@ HeroExchangeArmy * HeroExchangeMap::tryUpgrade(
for(auto & creatureToBuy : buyArmy) for(auto & creatureToBuy : buyArmy)
{ {
auto targetSlot = target->getSlotFor(creatureToBuy.creID.toCreature()); auto targetSlot = target->getSlotFor(creatureToBuy.creID.toCreature());
if (targetSlot.validSlot())
target->addToSlot(targetSlot, creatureToBuy.creID, creatureToBuy.count); {
target->armyCost += creatureToBuy.creID.toCreature()->getFullRecruitCost() * creatureToBuy.count; target->addToSlot(targetSlot, creatureToBuy.creID, creatureToBuy.count);
target->requireBuyArmy = true; 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 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) CCreatureSet * DwellingActor::getDwellingCreatures(const CGDwelling * dwelling, bool waitForGrowth)

View File

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

View File

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

View File

@ -67,7 +67,7 @@ struct GraphPathNode
GrapthPathNodeType nodeType = GrapthPathNodeType::NORMAL; GrapthPathNodeType nodeType = GrapthPathNodeType::NORMAL;
GraphPathNodePointer previous; GraphPathNodePointer previous;
float cost = BAD_COST; float cost = BAD_COST;
uint64_t danger = 0; uint64_t linkDanger = 0;
const CGObjectInstance * obj = nullptr; const CGObjectInstance * obj = nullptr;
std::shared_ptr<SpecialAction> specialAction; 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 from = path.targetHero->visitablePos();
auto fromObj = actorObjectMap[path.targetHero]; 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( auto updated = target->tryAddConnection(
from, from,
pos, pos,
@ -220,7 +220,7 @@ void ObjectGraphCalculator::calculateConnections(const int3 & pos, std::vector<A
continue; continue;
} }
auto danger = ai->pathfinder->getStorage()->evaluateDanger(pos2, path1.targetHero, true); auto danger = ai->dangerEvaluator->evaluateDanger(pos2, path1.targetHero, true);
auto updated = target->tryAddConnection( auto updated = target->tryAddConnection(
pos1, pos1,
@ -321,7 +321,7 @@ void ObjectGraphCalculator::addObjectActor(const CGObjectInstance * obj)
void ObjectGraphCalculator::addJunctionActor(const int3 & visitablePos, bool isVirtualBoat) 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 internalCb = temporaryActorHeroes.front()->cb;
auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique<CGHeroInstance>(internalCb)).get(); auto objectActor = temporaryActorHeroes.emplace_back(std::make_unique<CGHeroInstance>(internalCb)).get();

View File

@ -164,7 +164,7 @@ namespace AIPathfinding
if(hero->canCastThisSpell(summonBoatSpell) if(hero->canCastThisSpell(summonBoatSpell)
&& hero->getSpellSchoolLevel(summonBoatSpell) >= MasteryLevel::ADVANCED) && hero->getSpellSchoolLevel(summonBoatSpell) >= MasteryLevel::ADVANCED)
{ {
// TODO: For lower school level we might need to check the existance of some boat // TODO: For lower school level we might need to check the existence of some boat
summonableVirtualBoats[hero] = std::make_shared<SummonBoatAction>(); summonableVirtualBoats[hero] = std::make_shared<SummonBoatAction>();
} }
} }

View File

@ -11,9 +11,11 @@
#include "AIMovementAfterDestinationRule.h" #include "AIMovementAfterDestinationRule.h"
#include "../Actions/BattleAction.h" #include "../Actions/BattleAction.h"
#include "../Actions/QuestAction.h" #include "../Actions/QuestAction.h"
#include "../Actions/WhirlpoolAction.h"
#include "../../Goals/Invalid.h" #include "../../Goals/Invalid.h"
#include "AIPreviousNodeRule.h" #include "AIPreviousNodeRule.h"
#include "../../../../lib/pathfinder/PathfinderOptions.h" #include "../../../../lib/pathfinder/PathfinderOptions.h"
#include "../../../../lib/pathfinder/CPathfinder.h"
namespace NKAI namespace NKAI
{ {
@ -34,7 +36,7 @@ namespace AIPathfinding
const PathfinderConfig * pathfinderConfig, const PathfinderConfig * pathfinderConfig,
CPathfinderHelper * pathfinderHelper) const CPathfinderHelper * pathfinderHelper) const
{ {
if(nodeStorage->isMovementIneficient(source, destination)) if(nodeStorage->isMovementInefficient(source, destination))
{ {
destination.node->locked = true; destination.node->locked = true;
destination.blocked = true; destination.blocked = true;
@ -225,7 +227,7 @@ namespace AIPathfinding
return false; 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) if(danger)
{ {
@ -311,7 +313,7 @@ namespace AIPathfinding
} }
auto hero = nodeStorage->getHero(source.node); 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 actualArmyValue = srcNode->actor->armyValue - srcNode->armyLoss;
uint64_t loss = nodeStorage->evaluateArmyLoss(hero, actualArmyValue, danger); 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" #include "../../lib/CRandomGenerator.h"
CStupidAI::CStupidAI() CStupidAI::CStupidAI()
: side(-1) : side(BattleSide::NONE)
, wasWaitingForRealize(false) , wasWaitingForRealize(false)
, wasUnlockingGs(false) , wasUnlockingGs(false)
{ {
@ -262,7 +262,7 @@ void CStupidAI::battleStacksEffectsSet(const BattleID & battleID, const SetStack
print("battleStacksEffectsSet called"); 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"); print("battleStart called");
side = Side; side = Side;
@ -296,7 +296,11 @@ BattleAction CStupidAI::goTowards(const BattleID & battleID, const CStack * stac
for(auto hex : hexes) for(auto hex : hexes)
{ {
if(vstd::contains(avHexes, hex)) if(vstd::contains(avHexes, hex))
{
if(stack->position == hex)
return BattleAction::makeDefend(stack);
return BattleAction::makeMove(stack, hex); return BattleAction::makeMove(stack, hex);
}
if(stack->coversPos(hex)) if(stack->coversPos(hex))
{ {
@ -336,7 +340,11 @@ BattleAction CStupidAI::goTowards(const BattleID & battleID, const CStack * stac
} }
if(vstd::contains(avHexes, currentDest)) if(vstd::contains(avHexes, currentDest))
{
if(stack->position == currentDest)
return BattleAction::makeDefend(stack);
return BattleAction::makeMove(stack, currentDest); return BattleAction::makeMove(stack, currentDest);
}
currentDest = reachability.predecessors[currentDest]; currentDest = reachability.predecessors[currentDest];
} }

View File

@ -17,7 +17,7 @@ class EnemyInfo;
class CStupidAI : public CBattleGameInterface class CStupidAI : public CBattleGameInterface
{ {
int side; BattleSide side;
std::shared_ptr<CBattleCallback> cb; std::shared_ptr<CBattleCallback> cb;
std::shared_ptr<Environment> env; std::shared_ptr<Environment> env;
@ -47,7 +47,7 @@ public:
void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) override; void battleSpellCast(const BattleID & battleID, const BattleSpellCast *sc) override;
void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) override;//called when a specific effect is set to stacks void battleStacksEffectsSet(const BattleID & battleID, const SetStackEffect & sse) override;//called when a specific effect is set to stacks
//void battleTriggerEffect(const BattleTriggerEffect & bte) override; //void battleTriggerEffect(const BattleTriggerEffect & bte) override;
void battleStart(const 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 void battleCatapultAttacked(const BattleID & battleID, const CatapultAttack & ca) override; //called when catapult makes an attack
private: 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/UnlockGuard.h"
#include "../../lib/CConfigHandler.h" #include "../../lib/CConfigHandler.h"
#include "../../lib/CHeroHandler.h"
#include "../../lib/mapObjects/CBank.h"
#include "../../lib/mapObjects/CGTownInstance.h" #include "../../lib/mapObjects/CGTownInstance.h"
#include "../../lib/mapObjects/CQuest.h" #include "../../lib/mapObjects/CQuest.h"
#include "../../lib/mapping/CMapDefines.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 // TODO: Such information should be provided by pathfinder
// Tile must be free or with unoccupied boat // Tile must be free or with unoccupied boat
if(!t->blocked) if(!t->blocked())
{ {
return true; return true;
} }
@ -249,8 +247,8 @@ bool compareArmyStrength(const CArmedInstance * a1, const CArmedInstance * a2)
bool compareArtifacts(const CArtifactInstance * a1, const CArtifactInstance * a2) bool compareArtifacts(const CArtifactInstance * a1, const CArtifactInstance * a2)
{ {
auto art1 = a1->artType; auto art1 = a1->getType();
auto art2 = a2->artType; auto art2 = a2->getType();
if(art1->getPrice() == art2->getPrice()) if(art1->getPrice() == art2->getPrice())
return art1->valOfBonuses(BonusType::PRIMARY_SKILL) > art2->valOfBonuses(BonusType::PRIMARY_SKILL); return art1->valOfBonuses(BonusType::PRIMARY_SKILL) > art2->valOfBonuses(BonusType::PRIMARY_SKILL);

View File

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

View File

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

View File

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

View File

@ -13,6 +13,7 @@
#include "../../CCallback.h" #include "../../CCallback.h"
#include "../../lib/mapObjects/MapObjects.h" #include "../../lib/mapObjects/MapObjects.h"
#include "../../lib/entities/building/CBuilding.h"
bool BuildingManager::tryBuildThisStructure(const CGTownInstance * t, BuildingID building, unsigned int maxDays) bool BuildingManager::tryBuildThisStructure(const CGTownInstance * t, BuildingID building, unsigned int maxDays)
{ {
@ -22,13 +23,13 @@ bool BuildingManager::tryBuildThisStructure(const CGTownInstance * t, BuildingID
return false; return false;
} }
if (!vstd::contains(t->town->buildings, building)) if (!vstd::contains(t->getTown()->buildings, building))
return false; // no such building in town return false; // no such building in town
if (t->hasBuilt(building)) //Already built? Shouldn't happen in general if (t->hasBuilt(building)) //Already built? Shouldn't happen in general
return true; 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) auto toBuild = buildPtr->requirements.getFulfillmentCandidates([&](const BuildingID & buildID)
{ {
@ -50,7 +51,7 @@ bool BuildingManager::tryBuildThisStructure(const CGTownInstance * t, BuildingID
for (const auto & buildID : toBuild) 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); EBuildingState canBuild = cb->canBuildStructure(t, buildID);
if (canBuild == EBuildingState::ALLOWED) 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> 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> 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, 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, 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> 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, 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 }; BuildingID::MAGES_GUILD_4, BuildingID::MAGES_GUILD_5 };
@ -195,7 +196,7 @@ bool BuildingManager::getBuildingOptions(const CGTownInstance * t)
return true; return true;
//workaround for mantis #2696 - build capitol with separate algorithm if it is available //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)) if(tryBuildNextStructure(t, capitolAndRequirements))
return true; 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) //at the end, try to get and build any extra buildings with nonstandard slots (for example HotA 3rd level dwelling)
std::vector<BuildingID> extraBuildings; std::vector<BuildingID> extraBuildings;
for (auto buildingInfo : t->town->buildings) for (auto buildingInfo : t->getTown()->buildings)
{ {
if (buildingInfo.first > BuildingID::DWELL_UP2_FIRST) if (buildingInfo.first > BuildingID::DWELL_UP2_FIRST)
extraBuildings.push_back(buildingInfo.first); extraBuildings.push_back(buildingInfo.first);

View File

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

View File

@ -219,12 +219,6 @@ float TacticalAdvantageEngine::getTacticalAdvantage(const CArmedInstance * we, c
enemyFlyers->setValue(enemyStructure.flyers); enemyFlyers->setValue(enemyStructure.flyers);
enemySpeed->setValue(enemyStructure.maxSpeed); 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); const CGTownInstance * fort = dynamic_cast<const CGTownInstance *>(enemy);
if(fort) if(fort)
castleWalls->setValue(fort->fortLevel()); castleWalls->setValue(fort->fortLevel());

View File

@ -16,7 +16,6 @@
#include "../../lib/mapObjectConstructors/AObjectTypeHandler.h" #include "../../lib/mapObjectConstructors/AObjectTypeHandler.h"
#include "../../lib/mapObjectConstructors/CObjectClassesHandler.h" #include "../../lib/mapObjectConstructors/CObjectClassesHandler.h"
#include "../../lib/mapObjectConstructors/CBankInstanceConstructor.h" #include "../../lib/mapObjectConstructors/CBankInstanceConstructor.h"
#include "../../lib/mapObjects/CBank.h"
#include "../../lib/mapObjects/CGCreature.h" #include "../../lib/mapObjects/CGCreature.h"
#include "../../lib/mapObjects/CGDwelling.h" #include "../../lib/mapObjects/CGDwelling.h"
#include "../../lib/gameState/InfoAboutArmy.h" #include "../../lib/gameState/InfoAboutArmy.h"
@ -62,25 +61,6 @@ Goals::TSubgoal FuzzyHelper::chooseSolution(Goals::TGoalVec vec)
return result; 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) float FuzzyHelper::evaluate(Goals::VisitTile & g)
{ {
if(g.parent) if(g.parent)
@ -301,32 +281,13 @@ ui64 FuzzyHelper::evaluateDanger(const CGObjectInstance * obj, const VCAI * ai)
cb->getTownInfo(obj, iat); cb->getTownInfo(obj, iat);
return iat.army.getStrength(); return iat.army.getStrength();
} }
case Obj::MONSTER: default:
{
//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:
{ {
const CArmedInstance * a = dynamic_cast<const CArmedInstance *>(obj); 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 #pragma once
#include "FuzzyEngines.h" #include "FuzzyEngines.h"
VCMI_LIB_NAMESPACE_BEGIN
class CBank;
VCMI_LIB_NAMESPACE_END
class DLL_EXPORT FuzzyHelper class DLL_EXPORT FuzzyHelper
{ {
public: public:
@ -42,8 +36,6 @@ public:
float evaluate(Goals::AbstractGoal & g); float evaluate(Goals::AbstractGoal & g);
void setPriority(Goals::TSubgoal & g); void setPriority(Goals::TSubgoal & g);
ui64 estimateBankDanger(const CBank * bank); //TODO: move to another class?
Goals::TSubgoal chooseSolution(Goals::TGoalVec vec); Goals::TSubgoal chooseSolution(Goals::TGoalVec vec);
//std::shared_ptr<AbstractGoal> chooseSolution (std::vector<std::shared_ptr<AbstractGoal>> & 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