Browse Source

Merge pull request #5114 from vcmi/beta

Merge beta -> master
Ivan Savenko 10 months ago
parent
commit
1cbe1229ee
100 changed files with 2876 additions and 2407 deletions
  1. 5 3
      .github/ISSUE_TEMPLATE/bug_report.md
  2. 99 44
      .github/workflows/github.yml
  3. 1 0
      .gitignore
  4. 1 1
      .gitmodules
  5. 181 36
      AI/BattleAI/AttackPossibility.cpp
  6. 6 1
      AI/BattleAI/AttackPossibility.h
  7. 0 106
      AI/BattleAI/BattleAI.cbp
  8. 30 11
      AI/BattleAI/BattleAI.cpp
  9. 5 6
      AI/BattleAI/BattleAI.h
  10. 0 168
      AI/BattleAI/BattleAI.vcxproj
  11. 249 86
      AI/BattleAI/BattleEvaluator.cpp
  12. 20 21
      AI/BattleAI/BattleEvaluator.h
  13. 384 188
      AI/BattleAI/BattleExchangeVariant.cpp
  14. 15 9
      AI/BattleAI/BattleExchangeVariant.h
  15. 1 5
      AI/BattleAI/CMakeLists.txt
  16. 1 0
      AI/BattleAI/PotentialTargets.cpp
  17. 31 23
      AI/BattleAI/StackWithBonuses.cpp
  18. 51 24
      AI/BattleAI/StackWithBonuses.h
  19. 1 1
      AI/BattleAI/ThreatMap.cpp
  20. 1 1
      AI/BattleAI/ThreatMap.h
  21. 0 4
      AI/CMakeLists.txt
  22. 0 8
      AI/EmptyAI/CEmptyAI.cpp
  23. 0 3
      AI/EmptyAI/CEmptyAI.h
  24. 0 86
      AI/EmptyAI/EmptyAI.cbp
  25. 0 181
      AI/EmptyAI/EmptyAI.vcxproj
  26. 0 285
      AI/FuzzyLite.cbp
  27. 59 58
      AI/Nullkiller/AIGateway.cpp
  28. 2 26
      AI/Nullkiller/AIGateway.h
  29. 21 15
      AI/Nullkiller/AIUtility.cpp
  30. 2 22
      AI/Nullkiller/AIUtility.h
  31. 70 22
      AI/Nullkiller/Analyzers/ArmyManager.cpp
  32. 0 2
      AI/Nullkiller/Analyzers/ArmyManager.h
  33. 56 32
      AI/Nullkiller/Analyzers/BuildAnalyzer.cpp
  34. 1 1
      AI/Nullkiller/Analyzers/BuildAnalyzer.h
  35. 49 2
      AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp
  36. 2 0
      AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h
  37. 17 16
      AI/Nullkiller/Analyzers/HeroManager.cpp
  38. 2 4
      AI/Nullkiller/Analyzers/HeroManager.h
  39. 19 7
      AI/Nullkiller/Analyzers/ObjectClusterizer.cpp
  40. 35 12
      AI/Nullkiller/Behaviors/BuildingBehavior.cpp
  41. 10 9
      AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp
  42. 5 10
      AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp
  43. 40 21
      AI/Nullkiller/Behaviors/DefenceBehavior.cpp
  44. 14 28
      AI/Nullkiller/Behaviors/ExplorationBehavior.cpp
  45. 6 31
      AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp
  46. 65 25
      AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp
  47. 9 0
      AI/Nullkiller/Behaviors/StartupBehavior.cpp
  48. 1 10
      AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp
  49. 3 5
      AI/Nullkiller/CMakeLists.txt
  50. 1 8
      AI/Nullkiller/Engine/FuzzyEngines.cpp
  51. 17 43
      AI/Nullkiller/Engine/FuzzyHelper.cpp
  52. 0 8
      AI/Nullkiller/Engine/FuzzyHelper.h
  53. 183 30
      AI/Nullkiller/Engine/Nullkiller.cpp
  54. 2 1
      AI/Nullkiller/Engine/Nullkiller.h
  55. 581 132
      AI/Nullkiller/Engine/PriorityEvaluator.cpp
  56. 34 3
      AI/Nullkiller/Engine/PriorityEvaluator.h
  57. 31 43
      AI/Nullkiller/Engine/Settings.cpp
  58. 17 1
      AI/Nullkiller/Engine/Settings.h
  59. 1 0
      AI/Nullkiller/Goals/AbstractGoal.h
  60. 3 0
      AI/Nullkiller/Goals/AdventureSpellCast.cpp
  61. 3 3
      AI/Nullkiller/Goals/BuildThis.cpp
  62. 32 3
      AI/Nullkiller/Goals/BuyArmy.cpp
  63. 0 6
      AI/Nullkiller/Goals/CGoal.h
  64. 1 1
      AI/Nullkiller/Goals/CaptureObject.h
  65. 1 1
      AI/Nullkiller/Goals/CompleteQuest.cpp
  66. 5 2
      AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp
  67. 33 2
      AI/Nullkiller/Goals/ExecuteHeroChain.cpp
  68. 1 1
      AI/Nullkiller/Goals/ExploreNeighbourTile.cpp
  69. 6 3
      AI/Nullkiller/Goals/RecruitHero.cpp
  70. 1 0
      AI/Nullkiller/Goals/RecruitHero.h
  71. 2 6
      AI/Nullkiller/Goals/StayAtTown.cpp
  72. 15 5
      AI/Nullkiller/Helpers/ArmyFormation.cpp
  73. 4 2
      AI/Nullkiller/Helpers/ArmyFormation.h
  74. 6 6
      AI/Nullkiller/Helpers/ExplorationHelper.cpp
  75. 0 2
      AI/Nullkiller/Helpers/ExplorationHelper.h
  76. 122 47
      AI/Nullkiller/Pathfinding/AINodeStorage.cpp
  77. 18 21
      AI/Nullkiller/Pathfinding/AINodeStorage.h
  78. 4 2
      AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp
  79. 55 0
      AI/Nullkiller/Pathfinding/Actions/WhirlpoolAction.cpp
  80. 35 0
      AI/Nullkiller/Pathfinding/Actions/WhirlpoolAction.h
  81. 11 9
      AI/Nullkiller/Pathfinding/Actors.cpp
  82. 1 1
      AI/Nullkiller/Pathfinding/Actors.h
  83. 41 15
      AI/Nullkiller/Pathfinding/GraphPaths.cpp
  84. 1 1
      AI/Nullkiller/Pathfinding/GraphPaths.h
  85. 3 3
      AI/Nullkiller/Pathfinding/ObjectGraphCalculator.cpp
  86. 1 1
      AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp
  87. 5 3
      AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp
  88. 0 84
      AI/StupidAI/StupidAI.cbp
  89. 10 2
      AI/StupidAI/StupidAI.cpp
  90. 2 2
      AI/StupidAI/StupidAI.h
  91. 0 152
      AI/StupidAI/StupidAI.vcxproj
  92. 3 5
      AI/VCAI/AIUtility.cpp
  93. 0 18
      AI/VCAI/AIUtility.h
  94. 1 1
      AI/VCAI/ArmyManager.cpp
  95. 0 2
      AI/VCAI/ArmyManager.h
  96. 8 7
      AI/VCAI/BuildingManager.cpp
  97. 0 2
      AI/VCAI/BuildingManager.h
  98. 0 6
      AI/VCAI/FuzzyEngines.cpp
  99. 5 44
      AI/VCAI/FuzzyHelper.cpp
  100. 0 8
      AI/VCAI/FuzzyHelper.h

+ 5 - 3
.github/ISSUE_TEMPLATE/bug_report.md

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

+ 99 - 44
.github/workflows/github.yml

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

+ 1 - 0
.gitignore

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

+ 1 - 1
.gitmodules

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

+ 181 - 36
AI/BattleAI/AttackPossibility.cpp

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

+ 6 - 1
AI/BattleAI/AttackPossibility.h

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

+ 0 - 106
AI/BattleAI/BattleAI.cbp

@@ -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>

+ 30 - 11
AI/BattleAI/BattleAI.cpp

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

+ 5 - 6
AI/BattleAI/BattleAI.h

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

+ 0 - 168
AI/BattleAI/BattleAI.vcxproj

@@ -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>

+ 249 - 86
AI/BattleAI/BattleEvaluator.cpp

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

+ 20 - 21
AI/BattleAI/BattleEvaluator.h

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

+ 384 - 188
AI/BattleAI/BattleExchangeVariant.cpp

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

+ 15 - 9
AI/BattleAI/BattleExchangeVariant.h

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

+ 1 - 5
AI/BattleAI/CMakeLists.txt

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

+ 1 - 0
AI/BattleAI/PotentialTargets.cpp

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

+ 31 - 23
AI/BattleAI/StackWithBonuses.cpp

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

+ 51 - 24
AI/BattleAI/StackWithBonuses.h

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

+ 1 - 1
AI/BattleAI/ThreatMap.cpp

@@ -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.

+ 1 - 1
AI/BattleAI/ThreatMap.h

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

+ 0 - 4
AI/CMakeLists.txt

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

+ 0 - 8
AI/EmptyAI/CEmptyAI.cpp

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

+ 0 - 3
AI/EmptyAI/CEmptyAI.h

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

+ 0 - 86
AI/EmptyAI/EmptyAI.cbp

@@ -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>

+ 0 - 181
AI/EmptyAI/EmptyAI.vcxproj

@@ -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>

+ 0 - 285
AI/FuzzyLite.cbp

@@ -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>

+ 59 - 58
AI/Nullkiller/AIGateway.cpp

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

+ 2 - 26
AI/Nullkiller/AIGateway.h

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

+ 21 - 15
AI/Nullkiller/AIUtility.cpp

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

+ 2 - 22
AI/Nullkiller/AIUtility.h

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

+ 70 - 22
AI/Nullkiller/Analyzers/ArmyManager.cpp

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

+ 0 - 2
AI/Nullkiller/Analyzers/ArmyManager.h

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

+ 56 - 32
AI/Nullkiller/Analyzers/BuildAnalyzer.cpp

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

+ 1 - 1
AI/Nullkiller/Analyzers/BuildAnalyzer.h

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

+ 49 - 2
AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp

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

+ 2 - 0
AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.h

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

+ 17 - 16
AI/Nullkiller/Analyzers/HeroManager.cpp

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

+ 2 - 4
AI/Nullkiller/Analyzers/HeroManager.h

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

+ 19 - 7
AI/Nullkiller/Analyzers/ObjectClusterizer.cpp

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

+ 35 - 12
AI/Nullkiller/Behaviors/BuildingBehavior.cpp

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

+ 10 - 9
AI/Nullkiller/Behaviors/BuyArmyBehavior.cpp

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

+ 5 - 10
AI/Nullkiller/Behaviors/CaptureObjectsBehavior.cpp

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

+ 40 - 21
AI/Nullkiller/Behaviors/DefenceBehavior.cpp

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

+ 14 - 28
AI/Nullkiller/Behaviors/ExplorationBehavior.cpp

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

+ 6 - 31
AI/Nullkiller/Behaviors/GatherArmyBehavior.cpp

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

+ 65 - 25
AI/Nullkiller/Behaviors/RecruitHeroBehavior.cpp

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

+ 9 - 0
AI/Nullkiller/Behaviors/StartupBehavior.cpp

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

+ 1 - 10
AI/Nullkiller/Behaviors/StayAtTownBehavior.cpp

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

+ 3 - 5
AI/Nullkiller/CMakeLists.txt

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

+ 1 - 8
AI/Nullkiller/Engine/FuzzyEngines.cpp

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

+ 17 - 43
AI/Nullkiller/Engine/FuzzyHelper.cpp

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

+ 0 - 8
AI/Nullkiller/Engine/FuzzyHelper.h

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

+ 183 - 30
AI/Nullkiller/Engine/Nullkiller.cpp

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

+ 2 - 1
AI/Nullkiller/Engine/Nullkiller.h

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

+ 581 - 132
AI/Nullkiller/Engine/PriorityEvaluator.cpp

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

+ 34 - 3
AI/Nullkiller/Engine/PriorityEvaluator.h

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

+ 31 - 43
AI/Nullkiller/Engine/Settings.cpp

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

+ 17 - 1
AI/Nullkiller/Engine/Settings.h

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

+ 1 - 0
AI/Nullkiller/Goals/AbstractGoal.h

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

+ 3 - 0
AI/Nullkiller/Goals/AdventureSpellCast.cpp

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

+ 3 - 3
AI/Nullkiller/Goals/BuildThis.cpp

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

+ 32 - 3
AI/Nullkiller/Goals/BuyArmy.cpp

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

+ 0 - 6
AI/Nullkiller/Goals/CGoal.h

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

+ 1 - 1
AI/Nullkiller/Goals/CaptureObject.h

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

+ 1 - 1
AI/Nullkiller/Goals/CompleteQuest.cpp

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

+ 5 - 2
AI/Nullkiller/Goals/ExchangeSwapTownHeroes.cpp

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

+ 33 - 2
AI/Nullkiller/Goals/ExecuteHeroChain.cpp

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

+ 1 - 1
AI/Nullkiller/Goals/ExploreNeighbourTile.cpp

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

+ 6 - 3
AI/Nullkiller/Goals/RecruitHero.cpp

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

+ 1 - 0
AI/Nullkiller/Goals/RecruitHero.h

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

+ 2 - 6
AI/Nullkiller/Goals/StayAtTown.cpp

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

+ 15 - 5
AI/Nullkiller/Helpers/ArmyFormation.cpp

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

+ 4 - 2
AI/Nullkiller/Helpers/ArmyFormation.h

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

+ 6 - 6
AI/Nullkiller/Helpers/ExplorationHelper.cpp

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

+ 0 - 2
AI/Nullkiller/Helpers/ExplorationHelper.h

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

+ 122 - 47
AI/Nullkiller/Pathfinding/AINodeStorage.cpp

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

+ 18 - 21
AI/Nullkiller/Pathfinding/AINodeStorage.h

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

+ 4 - 2
AI/Nullkiller/Pathfinding/AIPathfinderConfig.cpp

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

+ 55 - 0
AI/Nullkiller/Pathfinding/Actions/WhirlpoolAction.cpp

@@ -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());
+}*/
+
+}

+ 35 - 0
AI/Nullkiller/Pathfinding/Actions/WhirlpoolAction.h

@@ -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;
+	};
+}
+}

+ 11 - 9
AI/Nullkiller/Pathfinding/Actors.cpp

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

+ 1 - 1
AI/Nullkiller/Pathfinding/Actors.h

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

+ 41 - 15
AI/Nullkiller/Pathfinding/GraphPaths.cpp

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

+ 1 - 1
AI/Nullkiller/Pathfinding/GraphPaths.h

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

+ 3 - 3
AI/Nullkiller/Pathfinding/ObjectGraphCalculator.cpp

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

+ 1 - 1
AI/Nullkiller/Pathfinding/Rules/AILayerTransitionRule.cpp

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

+ 5 - 3
AI/Nullkiller/Pathfinding/Rules/AIMovementAfterDestinationRule.cpp

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

+ 0 - 84
AI/StupidAI/StupidAI.cbp

@@ -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>

+ 10 - 2
AI/StupidAI/StupidAI.cpp

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

+ 2 - 2
AI/StupidAI/StupidAI.h

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

+ 0 - 152
AI/StupidAI/StupidAI.vcxproj

@@ -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>

+ 3 - 5
AI/VCAI/AIUtility.cpp

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

+ 0 - 18
AI/VCAI/AIUtility.h

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

+ 1 - 1
AI/VCAI/ArmyManager.cpp

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

+ 0 - 2
AI/VCAI/ArmyManager.h

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

+ 8 - 7
AI/VCAI/BuildingManager.cpp

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

+ 0 - 2
AI/VCAI/BuildingManager.h

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

+ 0 - 6
AI/VCAI/FuzzyEngines.cpp

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

+ 5 - 44
AI/VCAI/FuzzyHelper.cpp

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

+ 0 - 8
AI/VCAI/FuzzyHelper.h

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

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