浏览代码

Refactoring Battle AI.
Divide BattleAI on the smaller files.

Michał Kalinowski 9 年之前
父节点
当前提交
18f8ca3cd0

+ 66 - 0
AI/BattleAI/AttackPossibility.cpp

@@ -0,0 +1,66 @@
+/*
+ * AttackPossibility.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 "AttackPossibility.h"
+
+int AttackPossibility::damageDiff() const
+{
+	if (!priorities)
+		priorities = new Priorities;
+	const auto dealtDmgValue = priorities->stackEvaluator(enemy) * damageDealt;
+	const auto receivedDmgValue = priorities->stackEvaluator(attack.attacker) * damageReceived;
+	return dealtDmgValue - receivedDmgValue;
+}
+
+int AttackPossibility::attackValue() const
+{
+	return damageDiff() + tacticImpact;
+}
+
+AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo &AttackInfo, const HypotheticChangesToBattleState &state, BattleHex hex)
+{
+	auto attacker = AttackInfo.attacker;
+	auto enemy = AttackInfo.defender;
+
+	const int remainingCounterAttacks = getValOr(state.counterAttacksLeft, enemy, enemy->counterAttacksRemaining());
+	const bool counterAttacksBlocked = attacker->hasBonusOfType(Bonus::BLOCKS_RETALIATION) || enemy->hasBonusOfType(Bonus::NO_RETALIATION);
+	const int totalAttacks = 1 + AttackInfo.attackerBonuses->getBonuses(Selector::type(Bonus::ADDITIONAL_ATTACK), (Selector::effectRange (Bonus::NO_LIMIT).Or(Selector::effectRange(Bonus::ONLY_MELEE_FIGHT))))->totalValue();
+
+	AttackPossibility ap = {enemy, hex, AttackInfo, 0, 0, 0};
+
+	auto curBai = AttackInfo; //we'll modify here the stack counts
+	for(int i  = 0; i < totalAttacks; i++)
+	{
+		std::pair<ui32, ui32> retaliation(0,0);
+		auto attackDmg = getCbc()->battleEstimateDamage(CRandomGenerator::getDefault(), curBai, &retaliation);
+		ap.damageDealt = (attackDmg.first + attackDmg.second) / 2;
+		ap.damageReceived = (retaliation.first + retaliation.second) / 2;
+
+		if(remainingCounterAttacks <= i || counterAttacksBlocked)
+			ap.damageReceived = 0;
+
+		curBai.attackerCount = attacker->count - attacker->countKilledByAttack(ap.damageReceived).first;
+		curBai.defenderCount = enemy->count - enemy->countKilledByAttack(ap.damageDealt).first;
+		if(!curBai.attackerCount)
+			break;
+		//TODO what about defender? should we break? but in pessimistic scenario defender might be alive
+	}
+
+	//TODO other damage related to attack (eg. fire shield and other abilities)
+
+	//Limit damages by total stack health
+	vstd::amin(ap.damageDealt, enemy->count * enemy->MaxHealth() - (enemy->MaxHealth() - enemy->firstHPleft));
+	vstd::amin(ap.damageReceived, attacker->count * attacker->MaxHealth() - (attacker->MaxHealth() - attacker->firstHPleft));
+
+	return ap;
+}
+
+
+Priorities* AttackPossibility::priorities = nullptr;

+ 50 - 0
AI/BattleAI/AttackPossibility.h

@@ -0,0 +1,50 @@
+/*
+ * AttackPossibility.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+#include "../../lib/BattleState.h"
+#include "CCallback.h"
+#include "common.h"
+
+
+struct HypotheticChangesToBattleState
+{
+	std::map<const CStack *, const IBonusBearer *> bonusesOfStacks;
+	std::map<const CStack *, int> counterAttacksLeft;
+};
+
+class Priorities
+{
+public:
+	std::vector<double> resourceTypeBaseValues;
+	std::function<double(const CStack *)> stackEvaluator;
+	Priorities()
+	{
+		//        range::copy(VLC->objh->resVals, std::back_inserter(resourceTypeBaseValues));
+		stackEvaluator = [](const CStack*){ return 1.0; };
+	}
+};
+
+class AttackPossibility
+{
+public:
+	const CStack *enemy; //redundant (to attack.defender) but looks nice
+	BattleHex tile; //tile from which we attack
+	BattleAttackInfo attack;
+
+	int damageDealt;
+	int damageReceived; //usually by counter-attack
+	int tacticImpact;
+
+	int damageDiff() const;
+	int attackValue() const;
+
+	static AttackPossibility evaluate(const BattleAttackInfo &AttackInfo, const HypotheticChangesToBattleState &state, BattleHex hex);
+	static Priorities * priorities;
+};

+ 128 - 507
AI/BattleAI/BattleAI.cpp

@@ -1,88 +1,47 @@
+/*
+	 * BattleAI.cpp, part of VCMI engine
+	 *
+	 * Authors: listed in file AUTHORS in main folder
+	 *
+	 * License: GNU General Public License v2.0 or later
+	 * Full text of license available in license.txt file, in main folder
+	 *
+	 */
 #include "StdInc.h"
-#include "../../lib/AI_Base.h"
 #include "BattleAI.h"
-#include "../../lib/BattleState.h"
-#include "../../CCallback.h"
-#include "../../lib/CCreatureHandler.h"
+#include "StackWithBonuses.h"
+#include "EnemyInfo.h"
 #include "../../lib/spells/CSpellHandler.h"
-#include "../../lib/VCMI_Lib.h"
 
-using boost::optional;
-static std::shared_ptr<CBattleCallback> cbc;
 
-#define LOGL(text) print(text)
-#define LOGFL(text, formattingEl) print(boost::str(boost::format(text) % formattingEl))
-
-struct Priorities
-{
-	double manaValue;
-	double generalResourceValueModifier;
-	std::vector<double> resourceTypeBaseValues;
-	std::function<double(const CStack *)> stackEvaluator;
-
-
-	Priorities()
-	{
-		manaValue = 0.;
-		generalResourceValueModifier = 1.;
-		range::copy(VLC->objh->resVals, std::back_inserter(resourceTypeBaseValues));
-		stackEvaluator = [](const CStack*){ return 1.0; };
-	}
-};
-
-Priorities *priorities = nullptr;
-
-
-namespace {
-
-int distToNearestNeighbour(BattleHex hex, const ReachabilityInfo::TDistances& dists, BattleHex *chosenHex = nullptr)
-{
-	int ret = 1000000;
-	for(BattleHex n : hex.neighbouringTiles())
-	{
-		if(dists[n] >= 0 && dists[n] < ret)
-		{
-			ret = dists[n];
-			if(chosenHex)
-				*chosenHex = n;
-		}
-	}
-
-	return ret;
-}
-
-bool isCloser(const EnemyInfo & ei1, const EnemyInfo & ei2, const ReachabilityInfo::TDistances & dists)
-{
-	return distToNearestNeighbour(ei1.s->position, dists) < distToNearestNeighbour(ei2.s->position, dists);
-}
-
-}
-
-template <typename Container, typename Pred>
-auto sum(const Container & c, Pred p) -> decltype(p(*std::begin(c)))
-{
-	double ret = 0;
-	for(const auto &element : c)
-	{
-		ret += p(element);
-	}
-
-	return ret;
-}
 
+/*
+	//
+	// //set has its own order, so remove_if won't work. TODO - reuse for map
+	// template<typename Elem, typename Predicate>
+	// void erase_if(std::set<Elem> &setContainer, Predicate pred)
+	// {
+	// 	auto itr = setContainer.begin();
+	// 	auto endItr = setContainer.end();
+	// 	while(itr != endItr)
+	// 	{
+	// 		auto tmpItr = itr++;
+	// 		if(pred(*tmpItr))
+	// 			setContainer.erase(tmpItr);
+	// 	}
+	// }
+	*/
 
+#define LOGL(text) print(text)
+#define LOGFL(text, formattingEl) print(boost::str(boost::format(text) % formattingEl))
 
 CBattleAI::CBattleAI(void)
 	: side(-1)
 {
-	print("created");
 }
 
-
 CBattleAI::~CBattleAI(void)
 {
-	print("destroyed");
-
 	if(cb)
 	{
 		//Restore previous state of CB - it may be shared with the main AI (like VCAI)
@@ -93,10 +52,9 @@ CBattleAI::~CBattleAI(void)
 
 void CBattleAI::init(std::shared_ptr<CBattleCallback> CB)
 {
-	print("init called, saving ptr to IBattleCallback");
-	cbc = cb = CB;
+	setCbc(CB);
+	cb = CB;
 	playerID = *CB->getPlayerID();; //TODO should be sth in callback
-
 	wasWaitingForRealize = cb->waitTillRealize;
 	wasUnlockingGs = CB->unlockGsWhenWaiting;
 	CB->waitTillRealize = true;
@@ -106,14 +64,11 @@ void CBattleAI::init(std::shared_ptr<CBattleCallback> CB)
 BattleAction CBattleAI::activeStack( const CStack * stack )
 {
 	LOG_TRACE_PARAMS(logAi, "stack: %s", stack->nodeName())	;
-
-	cbc = cb; //TODO: make solid sure that AIs always use their callbacks (need to take care of event handlers too)
+	setCbc(cb); //TODO: make solid sure that AIs always use their callbacks (need to take care of event handlers too)
 	try
 	{
-		print("activeStack called for " + stack->nodeName());
 		if(stack->type->idNumber == CreatureID::CATAPULT)
 			return useCatapult(stack);
-
 		if(stack->hasBonusOfType(Bonus::SIEGE_WEAPON) && stack->hasBonusOfType(Bonus::HEALER))
 		{
 			auto healingTargets = cb->battleGetStacks(CBattleInfoEssentials::ONLY_MINE);
@@ -121,7 +76,6 @@ BattleAction CBattleAI::activeStack( const CStack * stack )
 			for(auto stack : healingTargets)
 				if(auto woundHp = stack->MaxHealth() - stack->firstHPleft)
 					woundHpToStack[woundHp] = stack;
-
 			if(woundHpToStack.empty())
 				return BattleAction::makeDefend(stack);
 			else
@@ -130,8 +84,7 @@ BattleAction CBattleAI::activeStack( const CStack * stack )
 
 		if(cb->battleCanCastSpell())
 			attemptCastingSpell();
-
-		if(auto ret = cbc->battleIsFinished())
+		if(auto ret = getCbc()->battleIsFinished())
 		{
 			//spellcast may finish battle
 			//send special preudo-action
@@ -142,9 +95,7 @@ BattleAction CBattleAI::activeStack( const CStack * stack )
 
 		if(auto action = considerFleeingOrSurrendering())
 			return *action;
-
 		PotentialTargets targets(stack);
-
 		if(targets.possibleAttacks.size())
 		{
 			auto hlp = targets.bestAction();
@@ -157,8 +108,8 @@ BattleAction CBattleAI::activeStack( const CStack * stack )
 		{
 			if(stack->waited())
 			{
-				ThreatMap threatsToUs(stack);
-				auto dists = cbc->battleGetDistances(stack);
+				//ThreatMap threatsToUs(stack); // These lines may be usefull but they are't used in the code.
+				auto dists = getCbc()->battleGetDistances(stack);
 				const EnemyInfo &ei= *range::min_element(targets.unreachableEnemies, std::bind(isCloser, _1, _2, std::ref(dists)));
 				if(distToNearestNeighbour(ei.s->position, dists) < GameConstants::BFIELD_SIZE)
 				{
@@ -175,105 +126,16 @@ BattleAction CBattleAI::activeStack( const CStack * stack )
 	{
 		logAi->error("Exception occurred in %s %s",__FUNCTION__, e.what());
 	}
-
 	return BattleAction::makeDefend(stack);
 }
 
-void CBattleAI::actionFinished(const BattleAction &action)
-{
-	print("actionFinished called");
-}
-
-void CBattleAI::actionStarted(const BattleAction &action)
-{
-	print("actionStarted called");
-}
-
-void CBattleAI::battleAttack(const BattleAttack *ba)
-{
-	print("battleAttack called");
-}
-
-void CBattleAI::battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa)
-{
-	print("battleStacksAttacked called");
-}
-
-void CBattleAI::battleEnd(const BattleResult *br)
-{
-	print("battleEnd called");
-}
-
-void CBattleAI::battleNewRoundFirst(int round)
-{
-	print("battleNewRoundFirst called");
-}
-
-void CBattleAI::battleNewRound(int round)
-{
-	print("battleNewRound called");
-}
-
-void CBattleAI::battleStackMoved(const CStack * stack, std::vector<BattleHex> dest, int distance)
-{
-	print("battleStackMoved called");;
-}
-
-void CBattleAI::battleSpellCast(const BattleSpellCast *sc)
-{
-	print("battleSpellCast called");
-}
-
-void CBattleAI::battleStacksEffectsSet(const SetStackEffect & sse)
-{
-	print("battleStacksEffectsSet called");
-}
-
-void CBattleAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side)
-{
-	print("battleStart called");
-	side = Side;
-}
-
-void CBattleAI::battleStacksHealedRes(const std::vector<std::pair<ui32, ui32> > & healedStacks, bool lifeDrain, bool tentHeal, si32 lifeDrainFrom)
-{
-	print("battleStacksHealedRes called");
-}
-
-void CBattleAI::battleNewStackAppeared(const CStack * stack)
-{
-	print("battleNewStackAppeared called");
-}
-
-void CBattleAI::battleObstaclesRemoved(const std::set<si32> & removedObstacles)
-{
-	print("battleObstaclesRemoved called");
-}
-
-void CBattleAI::battleCatapultAttacked(const CatapultAttack & ca)
-{
-	print("battleCatapultAttacked called");
-}
-
-void CBattleAI::battleStacksRemoved(const BattleStacksRemoved & bsr)
-{
-	print("battleStacksRemoved called");
-}
-
-void CBattleAI::print(const std::string &text) const
-{
-	logAi->trace("CBattleAI [%p]: %s", this, text);
-}
-
 BattleAction CBattleAI::goTowards(const CStack * stack, BattleHex destination)
 {
 	assert(destination.isValid());
 	auto avHexes = cb->battleGetAvailableHexes(stack, false);
 	auto reachability = cb->getReachability(stack);
-
 	if(vstd::contains(avHexes, destination))
 		return BattleAction::makeMove(stack, destination);
-
 	auto destNeighbours = destination.neighbouringTiles();
 	if(vstd::contains_if(destNeighbours, [&](BattleHex n) { return stack->coversPos(destination); }))
 	{
@@ -281,15 +143,11 @@ BattleAction CBattleAI::goTowards(const CStack * stack, BattleHex destination)
 		//We shouldn't even be here...
 		return BattleAction::makeDefend(stack);
 	}
-
 	vstd::erase_if(destNeighbours, [&](BattleHex hex){ return !reachability.accessibility.accessible(hex, stack); });
-
 	if(!avHexes.size() || !destNeighbours.size()) //we are blocked or dest is blocked
 	{
-		print("goTowards: Stack cannot move! That's " + stack->nodeName());
 		return BattleAction::makeDefend(stack);
 	}
-
 	if(stack->hasBonusOfType(Bonus::FLYING))
 	{
 		// Flying stack doesn't go hex by hex, so we can't backtrack using predecessors.
@@ -297,13 +155,9 @@ BattleAction CBattleAI::goTowards(const CStack * stack, BattleHex destination)
 		auto distToDestNeighbour = [&](BattleHex hex) -> int
 		{
 			auto nearestNeighbourToHex = vstd::minElementByFun(destNeighbours, [&](BattleHex a)
-			{
-				return BattleHex::getDistance(a, hex);
-			});
-
+			{return BattleHex::getDistance(a, hex);});
 			return BattleHex::getDistance(*nearestNeighbourToHex, hex);
 		};
-
 		auto nearestAvailableHex = vstd::minElementByFun(avHexes, distToDestNeighbour);
 		return BattleAction::makeMove(stack, *nearestAvailableHex);
 	}
@@ -312,17 +166,14 @@ BattleAction CBattleAI::goTowards(const CStack * stack, BattleHex destination)
 		BattleHex bestNeighbor = destination;
 		if(distToNearestNeighbour(destination, reachability.distances, &bestNeighbor) > GameConstants::BFIELD_SIZE)
 		{
-			print("goTowards: Cannot reach");
 			return BattleAction::makeDefend(stack);
 		}
-
 		BattleHex currentDest = bestNeighbor;
 		while(1)
 		{
 			assert(currentDest.isValid());
 			if(vstd::contains(avHexes, currentDest))
 				return BattleAction::makeMove(stack, currentDest);
-
 			currentDest = reachability.predecessors[currentDest];
 		}
 	}
@@ -333,6 +184,7 @@ BattleAction CBattleAI::useCatapult(const CStack * stack)
 	throw std::runtime_error("The method or operation is not implemented.");
 }
 
+
 enum SpellTypes
 {
 	OFFENSIVE_SPELL, TIMED_EFFECT, OTHER
@@ -345,74 +197,17 @@ SpellTypes spellType(const CSpell *spell)
 	if (spell->hasEffects())
 		return TIMED_EFFECT;
 	return OTHER;
-
 }
 
-struct PossibleSpellcast
-{
-	const CSpell *spell;
-	BattleHex dest;
-	si32 value;
-};
-
-struct CurrentOffensivePotential
-{
-	std::map<const CStack *, PotentialTargets> ourAttacks;
-	std::map<const CStack *, PotentialTargets> enemyAttacks;
-
-	CurrentOffensivePotential(ui8 side)
-	{
-		for(auto stack : cbc->battleGetStacks())
-		{
-			if(stack->attackerOwned == !side)
-				ourAttacks[stack] = PotentialTargets(stack);
-			else
-				enemyAttacks[stack] = PotentialTargets(stack);
-		}
-	}
-
-	int potentialValue()
-	{
-		int ourPotential = 0, enemyPotential = 0;
-		for(auto &p : ourAttacks)
-			ourPotential += p.second.bestAction().attackValue();
-
-		for(auto &p : enemyAttacks)
-			enemyPotential += p.second.bestAction().attackValue();
-
-		return ourPotential - enemyPotential;
-	}
-};
-
-
-//
-// //set has its own order, so remove_if won't work. TODO - reuse for map
-// template<typename Elem, typename Predicate>
-// void erase_if(std::set<Elem> &setContainer, Predicate pred)
-// {
-// 	auto itr = setContainer.begin();
-// 	auto endItr = setContainer.end();
-// 	while(itr != endItr)
-// 	{
-// 		auto tmpItr = itr++;
-// 		if(pred(*tmpItr))
-// 			setContainer.erase(tmpItr);
-// 	}
-// }
-
 void CBattleAI::attemptCastingSpell()
 {
 	LOGL("Casting spells sounds like fun. Let's see...");
-
-	auto hero = cb->battleGetMyHero();
-
-	//auto known = cb->battleGetFightingHero(side);
-
+	auto hero = cb->battleGetMyHero(); //auto known = cb->battleGetFightingHero(side);
 	//Get all spells we can cast
 	std::vector<const CSpell*> possibleSpells;
 	vstd::copy_if(VLC->spellh->objects, std::back_inserter(possibleSpells), [this] (const CSpell *s) -> bool
 	{
-		auto problem = cbc->battleCanCastThisSpell(s);
+		auto problem = getCbc()->battleCanCastThisSpell(s);
 		return problem == ESpellCastProblem::OK;
 	});
 	LOGFL("I can cast %d spells.", possibleSpells.size());
@@ -446,74 +241,59 @@ void CBattleAI::attemptCastingSpell()
 	{
 		const int skillLevel = hero->getSpellSchoolLevel(ps.spell);
 		const int spellPower = hero->getPrimSkillLevel(PrimarySkill::SPELL_POWER);
-
 		switch(spellType(ps.spell))
 		{
 		case OFFENSIVE_SPELL:
+		{
+			int damageDealt = 0, damageReceived = 0;
+			auto stacksSuffering = ps.spell->getAffectedStacks(cb.get(), ECastingMode::HERO_CASTING, hero, skillLevel, ps.dest);
+			if(stacksSuffering.empty())
+				return -1;
+			for(auto stack : stacksSuffering)
 			{
-				int damageDealt = 0, damageReceived = 0;
-
-				auto stacksSuffering = ps.spell->getAffectedStacks(cb.get(), ECastingMode::HERO_CASTING, hero, skillLevel, ps.dest);
-
-				if(stacksSuffering.empty())
-					return -1;
-
-				for(auto stack : stacksSuffering)
-				{
-					const int dmg = ps.spell->calculateDamage(hero, stack, skillLevel, spellPower);
-					if(stack->owner == playerID)
-						damageReceived += dmg;
-					else
-						damageDealt += dmg;
-				}
-
-				const int damageDiff = damageDealt - damageReceived * 10;
-
-				LOGFL("Casting %s on hex %d would deal { %d %d } damage points among %d stacks.",
-					ps.spell->name % ps.dest % damageDealt % damageReceived % stacksSuffering.size());
-				//TODO tactic effect too
-				return damageDiff;
+				const int dmg = ps.spell->calculateDamage(hero, stack, skillLevel, spellPower);
+				if(stack->owner == playerID)
+					damageReceived += dmg;
+				else
+					damageDealt += dmg;
 			}
+			const int damageDiff = damageDealt - damageReceived * 10;
+			LOGFL("Casting %s on hex %d would deal { %d %d } damage points among %d stacks.",
+				  ps.spell->name % ps.dest % damageDealt % damageReceived % stacksSuffering.size());
+			//TODO tactic effect too
+			return damageDiff;
+		}
 		case TIMED_EFFECT:
+		{
+			auto stacksAffected = ps.spell->getAffectedStacks(cb.get(), ECastingMode::HERO_CASTING, hero, skillLevel, ps.dest);
+			if(stacksAffected.empty())
+				return -1;
+			int totalGain = 0;
+			for(const CStack * sta : stacksAffected)
 			{
-				auto stacksAffected = ps.spell->getAffectedStacks(cb.get(), ECastingMode::HERO_CASTING, hero, skillLevel, ps.dest);
-
-				if(stacksAffected.empty())
-					return -1;
-
-				int totalGain = 0;
-
-				for(const CStack * sta : stacksAffected)
-				{
-					StackWithBonuses swb;
-					swb.stack = sta;
-
-					Bonus pseudoBonus;
-					pseudoBonus.sid = ps.spell->id;
-					pseudoBonus.val = skillLevel;
-					pseudoBonus.turnsRemain = 1; //TODO
-					CStack::stackEffectToFeature(swb.bonusesToAdd, pseudoBonus);
-
-					HypotheticChangesToBattleState state;
-					state.bonusesOfStacks[swb.stack] = &swb;
-
-					PotentialTargets pt(swb.stack, state);
-					auto newValue = pt.bestActionValue();
-					auto oldValue = valueOfStack[swb.stack];
-					auto gain = newValue - oldValue;
-					if(swb.stack->owner != playerID) //enemy
-						gain = -gain;
-
-					LOGFL("Casting %s on %s would improve the stack by %d points (from %d to %d)",
-						ps.spell->name % sta->nodeName() % (gain) % (oldValue) % (newValue));
-
-					totalGain += gain;
-				}
-
-				LOGFL("Total gain of cast %s at hex %d is %d", ps.spell->name % (ps.dest.hex) % (totalGain));
-
-				return totalGain;
+				StackWithBonuses swb;
+				swb.stack = sta;
+				Bonus pseudoBonus;
+				pseudoBonus.sid = ps.spell->id;
+				pseudoBonus.val = skillLevel;
+				pseudoBonus.turnsRemain = 1; //TODO
+				CStack::stackEffectToFeature(swb.bonusesToAdd, pseudoBonus);
+				HypotheticChangesToBattleState state;
+				state.bonusesOfStacks[swb.stack] = &swb;
+				PotentialTargets pt(swb.stack, state);
+				auto newValue = pt.bestActionValue();
+				auto oldValue = valueOfStack[swb.stack];
+				auto gain = newValue - oldValue;
+				if(swb.stack->owner != playerID) //enemy
+					gain = -gain;
+				LOGFL("Casting %s on %s would improve the stack by %d points (from %d to %d)",
+					  ps.spell->name % sta->nodeName() % (gain) % (oldValue) % (newValue));
+				totalGain += gain;
 			}
+
+			LOGFL("Total gain of cast %s at hex %d is %d", ps.spell->name % (ps.dest.hex) % (totalGain));
+			return totalGain;
+		}
 		default:
 			assert(0);
 			return 0;
@@ -522,22 +302,18 @@ void CBattleAI::attemptCastingSpell()
 
 	for(PossibleSpellcast & psc : possibleCasts)
 		psc.value = evaluateSpellcast(psc);
-
 	auto pscValue = [] (const PossibleSpellcast &ps) -> int
 	{
 		return ps.value;
 	};
-
 	auto castToPerform = *vstd::maxElementByFun(possibleCasts, pscValue);
 	LOGFL("Best spell is %s. Will cast.", castToPerform.spell->name);
-
 	BattleAction spellcast;
 	spellcast.actionType = Battle::HERO_SPELL;
 	spellcast.additionalInfo = castToPerform.spell->id;
 	spellcast.destinationTile = castToPerform.dest;
 	spellcast.side = side;
 	spellcast.stackNumber = (!side) ? -1 : -2;
-
 	cb->battleMakeAction(&spellcast);
 }
 
@@ -545,7 +321,6 @@ std::vector<BattleHex> CBattleAI::getTargetsToConsider(const CSpell * spell, con
 {
 	const CSpell::TargetInfo targetInfo(spell, caster->getSpellSchoolLevel(spell));
 	std::vector<BattleHex> ret;
-
 	if(targetInfo.massive || targetInfo.type == CSpell::NO_TARGET)
 	{
 		ret.push_back(BattleHex());
@@ -555,240 +330,86 @@ std::vector<BattleHex> CBattleAI::getTargetsToConsider(const CSpell * spell, con
 		switch(targetInfo.type)
 		{
 		case CSpell::CREATURE:
+		{
+			for(const CStack * stack : getCbc()->battleAliveStacks())
 			{
-				for(const CStack * stack : cbc->battleAliveStacks())
-				{
-					bool immune = ESpellCastProblem::OK != spell->isImmuneByStack(caster, stack);
-					bool casterStack = stack->owner == caster->getOwner();
-
-					if(!immune)
-						switch (spell->positiveness)
-						{
-						case CSpell::POSITIVE:
-							if(casterStack || targetInfo.smart)
-								ret.push_back(stack->position);
-							break;
-
-						case CSpell::NEUTRAL:
+				bool immune = ESpellCastProblem::OK != spell->isImmuneByStack(caster, stack);
+				bool casterStack = stack->owner == caster->getOwner();
+
+				if(!immune)
+					switch (spell->positiveness)
+					{
+					case CSpell::POSITIVE:
+						if(casterStack || targetInfo.smart)
 							ret.push_back(stack->position);
-							break;
-
-						case CSpell::NEGATIVE:
-							if(!casterStack || targetInfo.smart)
-								ret.push_back(stack->position);
-							break;
-						}
-				}
+						break;
+					case CSpell::NEUTRAL:
+						ret.push_back(stack->position);
+						break;
+					case CSpell::NEGATIVE:
+						if(!casterStack || targetInfo.smart)
+							ret.push_back(stack->position);
+						break;
+					}
 			}
+		}
 			break;
 		case CSpell::LOCATION:
-			{
-				for(int i = 0; i < GameConstants::BFIELD_SIZE; i++)
-					if(BattleHex(i).isAvailable())
-						ret.push_back(i);
-			}
+		{
+			for(int i = 0; i < GameConstants::BFIELD_SIZE; i++)
+				if(BattleHex(i).isAvailable())
+					ret.push_back(i);
+		}
 			break;
 
 		default:
 			break;
 		}
 	}
-
 	return ret;
 }
 
-boost::optional<BattleAction> CBattleAI::considerFleeingOrSurrendering()
+int CBattleAI::distToNearestNeighbour(BattleHex hex, const ReachabilityInfo::TDistances &dists, BattleHex *chosenHex)
 {
-	if(cb->battleCanSurrender(playerID))
-	{
-
-	}
-	if(cb->battleCanFlee())
-	{
-
-	}
-	return boost::none;
-}
-
-ThreatMap::ThreatMap(const CStack *Endangered) : endangered(Endangered)
-{
-	sufferedDamage.fill(0);
-
-	for(const CStack *enemy : cbc->battleGetStacks())
+	int ret = 1000000;
+	for(BattleHex n : hex.neighbouringTiles())
 	{
-		//Consider only stacks of different owner
-		if(enemy->attackerOwned == endangered->attackerOwned)
-			continue;
-
-		//Look-up which tiles can be melee-attacked
-		std::array<bool, GameConstants::BFIELD_SIZE> meleeAttackable;
-		meleeAttackable.fill(false);
-		auto enemyReachability = cbc->getReachability(enemy);
-		for(int i = 0; i < GameConstants::BFIELD_SIZE; i++)
-		{
-			if(enemyReachability.isReachable(i))
-			{
-				meleeAttackable[i] = true;
-				for(auto n : BattleHex(i).neighbouringTiles())
-					meleeAttackable[n] = true;
-			}
-		}
-
-		//Gather possible assaults
-		for(int i = 0; i < GameConstants::BFIELD_SIZE; i++)
+		if(dists[n] >= 0 && dists[n] < ret)
 		{
-			if(cbc->battleCanShoot(enemy, i))
-				threatMap[i].push_back(BattleAttackInfo(enemy, endangered, true));
-			else if(meleeAttackable[i])
-			{
-				BattleAttackInfo bai(enemy, endangered, false);
-				bai.chargedFields = std::max(BattleHex::getDistance(enemy->position, i) - 1, 0); //TODO check real distance (BFS), not just metric
-				threatMap[i].push_back(BattleAttackInfo(bai));
-			}
+			ret = dists[n];
+			if(chosenHex)
+				*chosenHex = n;
 		}
 	}
-
-	for(int i = 0; i < GameConstants::BFIELD_SIZE; i++)
-	{
-		sufferedDamage[i] = sum(threatMap[i], [](const BattleAttackInfo &bai) -> int
-		{
-			auto dmg = cbc->calculateDmgRange(bai);
-			return (dmg.first + dmg.second)/2;
-		});
-	}
+	return ret;
 }
 
-const TBonusListPtr StackWithBonuses::getAllBonuses(const CSelector &selector, const CSelector &limit, const CBonusSystemNode *root /*= nullptr*/, const std::string &cachingStr /*= ""*/) const
+void CBattleAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool Side)
 {
-	TBonusListPtr ret = std::make_shared<BonusList>();
-	const TBonusListPtr originalList = stack->getAllBonuses(selector, limit, root, cachingStr);
-	range::copy(*originalList, std::back_inserter(*ret));
-	for(auto &bonus : bonusesToAdd)
-	{
-		auto b = std::make_shared<Bonus>(bonus);
-		if(selector(b.get())  &&  (!limit || !limit(b.get())))
-			ret->push_back(b);
-	}
-
-	//TODO limiters?
-
-	return ret;
+	print("battleStart called");
+	side = Side;
 }
 
-int AttackPossibility::damageDiff() const
+bool CBattleAI::isCloser(const EnemyInfo &ei1, const EnemyInfo &ei2, const ReachabilityInfo::TDistances &dists)
 {
-	if (!priorities)
-		priorities = new Priorities;
-	const auto dealtDmgValue = priorities->stackEvaluator(enemy) * damageDealt;
-	const auto receivedDmgValue = priorities->stackEvaluator(attack.attacker) * damageReceived;
-	return dealtDmgValue - receivedDmgValue;
+	return distToNearestNeighbour(ei1.s->position, dists) < distToNearestNeighbour(ei2.s->position, dists);
 }
 
-int AttackPossibility::attackValue() const
+void CBattleAI::print(const std::string &text) const
 {
-	return damageDiff() + tacticImpact;
+	logAi->trace("CBattleAI [%p]: %s", this, text);
 }
 
-AttackPossibility AttackPossibility::evaluate(const BattleAttackInfo &AttackInfo, const HypotheticChangesToBattleState &state, BattleHex hex)
+boost::optional<BattleAction> CBattleAI::considerFleeingOrSurrendering()
 {
-	auto attacker = AttackInfo.attacker;
-	auto enemy = AttackInfo.defender;
-
-	const int remainingCounterAttacks = getValOr(state.counterAttacksLeft, enemy, enemy->counterAttacksRemaining());
-	const bool counterAttacksBlocked = attacker->hasBonusOfType(Bonus::BLOCKS_RETALIATION) || enemy->hasBonusOfType(Bonus::NO_RETALIATION);
-	const int totalAttacks = 1 + AttackInfo.attackerBonuses->getBonuses(Selector::type(Bonus::ADDITIONAL_ATTACK), (Selector::effectRange (Bonus::NO_LIMIT).Or(Selector::effectRange(Bonus::ONLY_MELEE_FIGHT))))->totalValue();
-
-	AttackPossibility ap = {enemy, hex, AttackInfo, 0, 0, 0};
-
-	auto curBai = AttackInfo; //we'll modify here the stack counts
-	for(int i  = 0; i < totalAttacks; i++)
+	if(cb->battleCanSurrender(playerID))
 	{
-		std::pair<ui32, ui32> retaliation(0,0);
-		auto attackDmg = cbc->battleEstimateDamage(CRandomGenerator::getDefault(), curBai, &retaliation);
-		ap.damageDealt = (attackDmg.first + attackDmg.second) / 2;
-		ap.damageReceived = (retaliation.first + retaliation.second) / 2;
-
-		if(remainingCounterAttacks <= i || counterAttacksBlocked)
-			ap.damageReceived = 0;
-
-		curBai.attackerCount = attacker->count - attacker->countKilledByAttack(ap.damageReceived).first;
-		curBai.defenderCount = enemy->count - enemy->countKilledByAttack(ap.damageDealt).first;
-		if(!curBai.attackerCount)
-			break;
-		//TODO what about defender? should we break? but in pessimistic scenario defender might be alive
 	}
-
-	//TODO other damage related to attack (eg. fire shield and other abilities)
-
-	//Limit damages by total stack health
-	vstd::amin(ap.damageDealt, enemy->count * enemy->MaxHealth() - (enemy->MaxHealth() - enemy->firstHPleft));
-	vstd::amin(ap.damageReceived, attacker->count * attacker->MaxHealth() - (attacker->MaxHealth() - attacker->firstHPleft));
-
-	return ap;
-}
-
-
-PotentialTargets::PotentialTargets(const CStack *attacker, const HypotheticChangesToBattleState &state /*= HypotheticChangesToBattleState()*/)
-{
-	auto dists = cbc->battleGetDistances(attacker);
-	auto avHexes = cbc->battleGetAvailableHexes(attacker, false);
-
-	for(const CStack *enemy : cbc->battleGetStacks())
+	if(cb->battleCanFlee())
 	{
-		//Consider only stacks of different owner
-		if(enemy->attackerOwned == attacker->attackerOwned)
-			continue;
-
-		auto GenerateAttackInfo = [&](bool shooting, BattleHex hex) -> AttackPossibility
-		{
-			auto bai = BattleAttackInfo(attacker, enemy, shooting);
-			bai.attackerBonuses = getValOr(state.bonusesOfStacks, bai.attacker, bai.attacker);
-			bai.defenderBonuses = getValOr(state.bonusesOfStacks, bai.defender, bai.defender);
-
-			if(hex.isValid())
-			{
-				assert(dists[hex] <= attacker->Speed());
-				bai.chargedFields = dists[hex];
-			}
-
-			return AttackPossibility::evaluate(bai, state, hex);
-		};
-
-		if(cbc->battleCanShoot(attacker, enemy->position))
-		{
-			possibleAttacks.push_back(GenerateAttackInfo(true, BattleHex::INVALID));
-		}
-		else
-		{
-			for(BattleHex hex : avHexes)
-				if(CStack::isMeleeAttackPossible(attacker, enemy, hex))
-					possibleAttacks.push_back(GenerateAttackInfo(false, hex));
-
-			if(!vstd::contains_if(possibleAttacks, [=](const AttackPossibility &pa) { return pa.enemy == enemy; }))
-				unreachableEnemies.push_back(enemy);
-		}
 	}
+	return boost::none;
 }
 
-AttackPossibility PotentialTargets::bestAction() const
-{
-	if(possibleAttacks.empty())
-		throw std::runtime_error("No best action, since we don't have any actions");
-
-	return *vstd::maxElementByFun(possibleAttacks, [](const AttackPossibility &ap) { return ap.attackValue(); } );
-}
-
-int PotentialTargets::bestActionValue() const
-{
-	if(possibleAttacks.empty())
-		return 0;
 
-	return bestAction().attackValue();
-}
 
-void EnemyInfo::calcDmg(const CStack * ourStack)
-{
-	TDmgRange retal, dmg = cbc->battleEstimateDamage(CRandomGenerator::getDefault(), ourStack, s, &retal);
-	adi = (dmg.first + dmg.second) / 2;
-	adr = (retal.first + retal.second) / 2;
-}

+ 64 - 117
AI/BattleAI/BattleAI.h

@@ -1,110 +1,55 @@
+/*
+ * BattleAI.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
 #pragma once
-
-#include "../../lib/BattleHex.h"
-#include "../../lib/HeroBonus.h"
-#include "../../lib/CBattleCallback.h"
+#include "../../lib/AI_Base.h"
+#include "PotentialTargets.h"
 
 class CSpell;
+class EnemyInfo;
 
-
-class StackWithBonuses : public IBonusBearer
-{
-public:
-	const CStack *stack;
-	mutable std::vector<Bonus> bonusesToAdd;
-
-	virtual const TBonusListPtr getAllBonuses(const CSelector &selector, const CSelector &limit, const CBonusSystemNode *root = nullptr, const std::string &cachingStr = "") const override;
-};
-
-struct EnemyInfo
+/*
+struct CurrentOffensivePotential
 {
-	const CStack * s;
-	int adi, adr;
-	std::vector<BattleHex> attackFrom; //for melee fight
-	EnemyInfo(const CStack * _s) : s(_s)
-	{}
-	void calcDmg(const CStack * ourStack);
+	std::map<const CStack *, PotentialTargets> ourAttacks;
+	std::map<const CStack *, PotentialTargets> enemyAttacks;
 
-	bool operator==(const EnemyInfo& ei) const
+	CurrentOffensivePotential(ui8 side)
 	{
-		return s == ei.s;
+		for(auto stack : cbc->battleGetStacks())
+		{
+			if(stack->attackerOwned == !side)
+				ourAttacks[stack] = PotentialTargets(stack);
+			else
+				enemyAttacks[stack] = PotentialTargets(stack);
+		}
 	}
-};
-
-
-//FIXME: unused function
-/*
-static bool willSecondHexBlockMoreEnemyShooters(const BattleHex &h1, const BattleHex &h2)
-{
-	int shooters[2] = {0}; //count of shooters on hexes
-
-	for(int i = 0; i < 2; i++)
-		BOOST_FOREACH(BattleHex neighbour, (i ? h2 : h1).neighbouringTiles())
-			if(const CStack *s = cbc->battleGetStackByPos(neighbour))
-				if(s->getCreature()->isShooting())
-						shooters[i]++;
-
-	return shooters[0] < shooters[1];
-}
-*/
-
-
-
-struct ThreatMap
-{
-	std::array<std::vector<BattleAttackInfo>, GameConstants::BFIELD_SIZE> threatMap; // [hexNr] -> enemies able to strike
-
-	const CStack *endangered;
-	std::array<int, GameConstants::BFIELD_SIZE> sufferedDamage;
-
-	ThreatMap(const CStack *Endangered);
-};
-
-struct HypotheticChangesToBattleState
-{
-	std::map<const CStack *, const IBonusBearer *> bonusesOfStacks;
-	std::map<const CStack *, int> counterAttacksLeft;
-};
-
-struct AttackPossibility
-{
-	const CStack *enemy; //redundant (to attack.defender) but looks nice
-	BattleHex tile; //tile from which we attack
-	BattleAttackInfo attack;
 
-	int damageDealt;
-	int damageReceived; //usually by counter-attack
-	int tacticImpact;
+	int potentialValue()
+	{
+		int ourPotential = 0, enemyPotential = 0;
+		for(auto &p : ourAttacks)
+			ourPotential += p.second.bestAction().attackValue();
 
-	int damageDiff() const;
-	int attackValue() const;
+		for(auto &p : enemyAttacks)
+			enemyPotential += p.second.bestAction().attackValue();
 
-	static AttackPossibility evaluate(const BattleAttackInfo &AttackInfo, const HypotheticChangesToBattleState &state, BattleHex hex);
+		return ourPotential - enemyPotential;
+	}
 };
+*/ // These lines may be usefull but they are't used in the code.
 
-template<typename Key, typename Val, typename Val2>
-const Val getValOr(const std::map<Key, Val> &Map, const Key &key, const Val2 defaultValue)
+struct PossibleSpellcast
 {
-	//returning references here won't work: defaultValue must be converted into Val, creating temporary
-	auto i = Map.find(key);
-	if(i != Map.end())
-		return i->second;
-	else
-		return defaultValue;
-}
-
-struct PotentialTargets
-{
-	std::vector<AttackPossibility> possibleAttacks;
-	std::vector<const CStack *> unreachableEnemies;
-
-	//std::function<AttackPossibility(bool,BattleHex)>  GenerateAttackInfo; //args: shooting, destHex
-
-	PotentialTargets(){};
-	PotentialTargets(const CStack *attacker, const HypotheticChangesToBattleState &state = HypotheticChangesToBattleState());
-
-	AttackPossibility bestAction() const;
-	int bestActionValue() const;
+	const CSpell *spell;
+	BattleHex dest;
+	si32 value;
 };
 
 class CBattleAI : public CBattleGameInterface
@@ -115,39 +60,41 @@ class CBattleAI : public CBattleGameInterface
 	//Previous setting of cb
 	bool wasWaitingForRealize, wasUnlockingGs;
 
-	void print(const std::string &text) const;
 public:
 	CBattleAI(void);
 	~CBattleAI(void);
 
 	void init(std::shared_ptr<CBattleCallback> CB) override;
-	void actionFinished(const BattleAction &action) override;//occurs AFTER every action taken by any stack or by the hero
-	void actionStarted(const BattleAction &action) override;//occurs BEFORE every action taken by any stack or by the hero
-	BattleAction activeStack(const CStack * stack) override; //called when it's turn of that stack
-
-	void battleAttack(const BattleAttack *ba) override; //called when stack is performing attack
-	void battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa) override; //called when stack receives damage (after battleAttack())
-	void battleEnd(const BattleResult *br) override;
-	//void battleResultsApplied() override; //called when all effects of last battle are applied
-	void battleNewRoundFirst(int round) override; //called at the beginning of each turn before changes are applied;
-	void battleNewRound(int round) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn
-	void battleStackMoved(const CStack * stack, std::vector<BattleHex> dest, int distance) override;
-	void battleSpellCast(const BattleSpellCast *sc) override;
-	void battleStacksEffectsSet(const SetStackEffect & sse) override;//called when a specific effect is set to stacks
-	//void battleTriggerEffect(const BattleTriggerEffect & bte) override;
-	void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) override; //called by engine when battle starts; side=0 - left, side=1 - right
-	void battleStacksHealedRes(const std::vector<std::pair<ui32, ui32> > & healedStacks, bool lifeDrain, bool tentHeal, si32 lifeDrainFrom) override; //called when stacks are healed / resurrected first element of pair - stack id, second - healed hp
-	void battleNewStackAppeared(const CStack * stack) override; //not called at the beginning of a battle or by resurrection; called eg. when elemental is summoned
-	void battleObstaclesRemoved(const std::set<si32> & removedObstacles) override; //called when a certain set  of obstacles is removed from batlefield; IDs of them are given
-	void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack
-	void battleStacksRemoved(const BattleStacksRemoved & bsr) override; //called when certain stack is completely removed from battlefield
+	void attemptCastingSpell();
 
+	BattleAction activeStack(const CStack * stack) override; //called when it's turn of that stack
 	BattleAction goTowards(const CStack * stack, BattleHex hex );
-	BattleAction useCatapult(const CStack * stack);
 
 	boost::optional<BattleAction> considerFleeingOrSurrendering();
 
-	void attemptCastingSpell();
 	std::vector<BattleHex> getTargetsToConsider(const CSpell *spell, const ISpellCaster * caster) const;
-};
+	static int distToNearestNeighbour(BattleHex hex, const ReachabilityInfo::TDistances& dists, BattleHex *chosenHex = nullptr);
+	static bool isCloser(const EnemyInfo & ei1, const EnemyInfo & ei2, const ReachabilityInfo::TDistances & dists);
 
+	void print(const std::string &text) const;
+	BattleAction useCatapult(const CStack *stack);
+	void battleStart(const CCreatureSet * army1, const CCreatureSet * army2, int3 tile, const CGHeroInstance * hero1, const CGHeroInstance * hero2, bool Side);
+	//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
+	//void battleStacksAttacked(const std::vector<BattleStackAttacked> & bsa) override; //called when stack receives damage (after battleAttack())
+	//void battleEnd(const BattleResult *br) override;
+	//void battleResultsApplied() override; //called when all effects of last battle are applied
+	//void battleNewRoundFirst(int round) override; //called at the beginning of each turn before changes are applied;
+	//void battleNewRound(int round) override; //called at the beginning of each turn, round=-1 is the tactic phase, round=0 is the first "normal" turn
+	//void battleStackMoved(const CStack * stack, std::vector<BattleHex> dest, int distance) override;
+	//void battleSpellCast(const BattleSpellCast *sc) override;
+	//void battleStacksEffectsSet(const SetStackEffect & sse) override;//called when a specific effect is set to stacks
+	//void battleTriggerEffect(const BattleTriggerEffect & bte) override;
+	//void battleStart(const CCreatureSet *army1, const CCreatureSet *army2, int3 tile, const CGHeroInstance *hero1, const CGHeroInstance *hero2, bool side) override; //called by engine when battle starts; side=0 - left, side=1 - right
+	//void battleStacksHealedRes(const std::vector<std::pair<ui32, ui32> > & healedStacks, bool lifeDrain, bool tentHeal, si32 lifeDrainFrom) override; //called when stacks are healed / resurrected first element of pair - stack id, second - healed hp
+	//void battleNewStackAppeared(const CStack * stack) override; //not called at the beginning of a battle or by resurrection; called eg. when elemental is summoned
+	//void battleObstaclesRemoved(const std::set<si32> & removedObstacles) override; //called when a certain set  of obstacles is removed from batlefield; IDs of them are given
+	//void battleCatapultAttacked(const CatapultAttack & ca) override; //called when catapult makes an attack
+	//void battleStacksRemoved(const BattleStacksRemoved & bsr) override; //called when certain stack is completely removed from battlefield
+};

+ 7 - 1
AI/BattleAI/CMakeLists.txt

@@ -6,7 +6,13 @@ include_directories(${Boost_INCLUDE_DIRS} ${CMAKE_HOME_DIRECTORY} ${CMAKE_HOME_D
 set(battleAI_SRCS
 		StdInc.cpp
 		BattleAI.cpp
+		StackWithBonuses.cpp
+		EnemyInfo.cpp
+		AttackPossibility.cpp
+		PotentialTargets.cpp
 		main.cpp
+		common.cpp
+		ThreatMap.cpp
 )
 
 add_library(BattleAI SHARED ${battleAI_SRCS})
@@ -16,6 +22,6 @@ set_target_properties(BattleAI PROPERTIES ${PCH_PROPERTIES})
 cotire(BattleAI)
 
 if (NOT APPLE) # Already inside vcmiclient bundle
-    install(TARGETS BattleAI RUNTIME DESTINATION ${AI_LIB_DIR} LIBRARY DESTINATION ${AI_LIB_DIR})
+	install(TARGETS BattleAI RUNTIME DESTINATION ${AI_LIB_DIR} LIBRARY DESTINATION ${AI_LIB_DIR})
 endif()
 

+ 21 - 0
AI/BattleAI/EnemyInfo.cpp

@@ -0,0 +1,21 @@
+/*
+ * EnemyInfo.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 "EnemyInfo.h"
+#include "CRandomGenerator.h"
+#include "CCallback.h"
+#include "common.h"
+
+void EnemyInfo::calcDmg(const CStack * ourStack)
+{
+	TDmgRange retal, dmg = getCbc()->battleEstimateDamage(CRandomGenerator::getDefault(), ourStack, s, &retal);
+	adi = (dmg.first + dmg.second) / 2;
+	adr = (retal.first + retal.second) / 2;
+}

+ 28 - 0
AI/BattleAI/EnemyInfo.h

@@ -0,0 +1,28 @@
+/*
+ * EnemyInfo.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 "BattleHex.h"
+
+class CStack;
+
+class EnemyInfo
+{
+public:
+	const CStack * s;
+	int adi, adr;
+	std::vector<BattleHex> attackFrom; //for melee fight
+	EnemyInfo(const CStack * _s) : s(_s)
+	{}
+	void calcDmg(const CStack * ourStack);
+	bool operator==(const EnemyInfo& ei) const
+	{
+		return s == ei.s;
+	}
+};

+ 71 - 0
AI/BattleAI/PotentialTargets.cpp

@@ -0,0 +1,71 @@
+/*
+ * PotentialTargets.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 "PotentialTargets.h"
+
+PotentialTargets::PotentialTargets(const CStack *attacker, const HypotheticChangesToBattleState &state /*= HypotheticChangesToBattleState()*/)
+{
+	auto dists = getCbc()->battleGetDistances(attacker);
+	auto avHexes = getCbc()->battleGetAvailableHexes(attacker, false);
+
+	for(const CStack *enemy : getCbc()->battleGetStacks())
+	{
+		//Consider only stacks of different owner
+		if(enemy->attackerOwned == attacker->attackerOwned)
+			continue;
+
+		auto GenerateAttackInfo = [&](bool shooting, BattleHex hex) -> AttackPossibility
+		{
+			auto bai = BattleAttackInfo(attacker, enemy, shooting);
+			bai.attackerBonuses = getValOr(state.bonusesOfStacks, bai.attacker, bai.attacker);
+			bai.defenderBonuses = getValOr(state.bonusesOfStacks, bai.defender, bai.defender);
+
+			if(hex.isValid())
+			{
+				assert(dists[hex] <= attacker->Speed());
+				bai.chargedFields = dists[hex];
+			}
+
+			return AttackPossibility::evaluate(bai, state, hex);
+		};
+
+		if(getCbc()->battleCanShoot(attacker, enemy->position))
+		{
+			possibleAttacks.push_back(GenerateAttackInfo(true, BattleHex::INVALID));
+		}
+		else
+		{
+			for(BattleHex hex : avHexes)
+				if(CStack::isMeleeAttackPossible(attacker, enemy, hex))
+					possibleAttacks.push_back(GenerateAttackInfo(false, hex));
+
+			if(!vstd::contains_if(possibleAttacks, [=](const AttackPossibility &pa) { return pa.enemy == enemy; }))
+				unreachableEnemies.push_back(enemy);
+		}
+	}
+}
+
+
+
+int PotentialTargets::bestActionValue() const
+{
+	if(possibleAttacks.empty())
+		return 0;
+
+	return bestAction().attackValue();
+}
+
+AttackPossibility PotentialTargets::bestAction() const
+{
+	if(possibleAttacks.empty())
+		throw std::runtime_error("No best action, since we don't have any actions");
+
+	return *vstd::maxElementByFun(possibleAttacks, [](const AttackPossibility &ap) { return ap.attackValue(); } );
+}

+ 26 - 0
AI/BattleAI/PotentialTargets.h

@@ -0,0 +1,26 @@
+/*
+ * PotentialTargets.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 "AttackPossibility.h"
+
+class PotentialTargets
+{
+public:
+	std::vector<AttackPossibility> possibleAttacks;
+	std::vector<const CStack *> unreachableEnemies;
+
+	//std::function<AttackPossibility(bool,BattleHex)>  GenerateAttackInfo; //args: shooting, destHex
+
+	PotentialTargets(){};
+	PotentialTargets(const CStack *attacker, const HypotheticChangesToBattleState &state = HypotheticChangesToBattleState());
+
+	AttackPossibility bestAction() const;
+	int bestActionValue() const;
+};

+ 28 - 0
AI/BattleAI/StackWithBonuses.cpp

@@ -0,0 +1,28 @@
+/*
+ * StackWithBonuses.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 "StackWithBonuses.h"
+#include "../../lib/BattleState.h"
+
+const TBonusListPtr StackWithBonuses::getAllBonuses(const CSelector &selector, const CSelector &limit,
+						    const CBonusSystemNode *root /*= nullptr*/, const std::string &cachingStr /*= ""*/) const
+{
+	TBonusListPtr ret = std::make_shared<BonusList>();
+	const TBonusListPtr originalList = stack->getAllBonuses(selector, limit, root, cachingStr);
+	range::copy(*originalList, std::back_inserter(*ret));
+	for(auto &bonus : bonusesToAdd)
+	{
+		auto b = std::make_shared<Bonus>(bonus);
+		if(selector(b.get())  &&  (!limit || !limit(b.get())))
+			ret->push_back(b);
+	}
+	//TODO limiters?
+	return ret;
+}

+ 23 - 0
AI/BattleAI/StackWithBonuses.h

@@ -0,0 +1,23 @@
+/*
+ * StackWithBonuses.h, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
+#pragma once
+#include "../../lib/HeroBonus.h"
+
+class CStack;
+
+class StackWithBonuses : public IBonusBearer
+{
+public:
+	const CStack *stack;
+	mutable std::vector<Bonus> bonusesToAdd;
+
+	virtual const TBonusListPtr getAllBonuses(const CSelector &selector, const CSelector &limit,
+						  const CBonusSystemNode *root = nullptr, const std::string &cachingStr = "") const override;
+};

+ 10 - 1
AI/BattleAI/StdInc.cpp

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

+ 10 - 2
AI/BattleAI/StdInc.h

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

+ 72 - 0
AI/BattleAI/ThreatMap.cpp

@@ -0,0 +1,72 @@
+/*
+ * ThreatMap.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 "ThreatMap.h"
+#include "StdInc.h"
+
+template <typename Container, typename Pred>
+auto sum(const Container & c, Pred p) -> decltype(p(*std::begin(c)))
+{
+	double ret = 0;
+	for(const auto &element : c)
+	{
+		ret += p(element);
+	}
+
+	return ret;
+}
+ThreatMap::ThreatMap(const CStack *Endangered) : endangered(Endangered)
+{
+	sufferedDamage.fill(0);
+
+	for(const CStack *enemy : getCbc()->battleGetStacks())
+	{
+		//Consider only stacks of different owner
+		if(enemy->attackerOwned == endangered->attackerOwned)
+			continue;
+
+		//Look-up which tiles can be melee-attacked
+		std::array<bool, GameConstants::BFIELD_SIZE> meleeAttackable;
+		meleeAttackable.fill(false);
+		auto enemyReachability = getCbc()->getReachability(enemy);
+		for(int i = 0; i < GameConstants::BFIELD_SIZE; i++)
+		{
+			if(enemyReachability.isReachable(i))
+			{
+				meleeAttackable[i] = true;
+				for(auto n : BattleHex(i).neighbouringTiles())
+					meleeAttackable[n] = true;
+			}
+		}
+
+		//Gather possible assaults
+		for(int i = 0; i < GameConstants::BFIELD_SIZE; i++)
+		{
+			if(getCbc()->battleCanShoot(enemy, i))
+				threatMap[i].push_back(BattleAttackInfo(enemy, endangered, true));
+			else if(meleeAttackable[i])
+			{
+				BattleAttackInfo bai(enemy, endangered, false);
+				bai.chargedFields = std::max(BattleHex::getDistance(enemy->position, i) - 1, 0); //TODO check real distance (BFS), not just metric
+				threatMap[i].push_back(BattleAttackInfo(bai));
+			}
+		}
+	}
+
+	for(int i = 0; i < GameConstants::BFIELD_SIZE; i++)
+	{
+		sufferedDamage[i] = sum(threatMap[i], [](const BattleAttackInfo &bai) -> int
+		{
+			auto dmg = getCbc()->calculateDmgRange(bai);
+			return (dmg.first + dmg.second)/2;
+		});
+	}
+}
+*/ // These lines may be usefull but they are't used in the code.

+ 26 - 0
AI/BattleAI/ThreatMap.h

@@ -0,0 +1,26 @@
+/*
+ * ThreatMap.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 "common.h"
+#include "../../lib/BattleState.h"
+#include "CCallback.h"
+/*
+class ThreatMap
+{
+public:
+	std::array<std::vector<BattleAttackInfo>, GameConstants::BFIELD_SIZE> threatMap; // [hexNr] -> enemies able to strike
+
+	const CStack *endangered;
+	std::array<int, GameConstants::BFIELD_SIZE> sufferedDamage;
+
+	ThreatMap(const CStack *Endangered);
+};*/ // These lines may be usefull but they are't used in the code.

+ 23 - 0
AI/BattleAI/common.cpp

@@ -0,0 +1,23 @@
+/*
+ * common.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 "common.h"
+
+std::shared_ptr<CBattleCallback> cbc;
+
+void setCbc(std::shared_ptr<CBattleCallback> cb)
+{
+	cbc = cb;
+}
+
+std::shared_ptr<CBattleCallback> getCbc()
+{
+	return cbc;
+}

+ 26 - 0
AI/BattleAI/common.h

@@ -0,0 +1,26 @@
+/*
+ * common.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
+
+class CBattleCallback;
+
+template<typename Key, typename Val, typename Val2>
+const Val getValOr(const std::map<Key, Val> &Map, const Key &key, const Val2 defaultValue)
+{
+	//returning references here won't work: defaultValue must be converted into Val, creating temporary
+	auto i = Map.find(key);
+	if(i != Map.end())
+		return i->second;
+	else
+		return defaultValue;
+}
+
+void setCbc(std::shared_ptr<CBattleCallback> cb);
+std::shared_ptr<CBattleCallback> getCbc();

+ 9 - 1
AI/BattleAI/main.cpp

@@ -1,5 +1,13 @@
+/*
+ * main.cpp, part of VCMI engine
+ *
+ * Authors: listed in file AUTHORS in main folder
+ *
+ * License: GNU General Public License v2.0 or later
+ * Full text of license available in license.txt file, in main folder
+ *
+ */
 #include "StdInc.h"
-
 #include "../../lib/AI_Base.h"
 #include "BattleAI.h"
 

+ 3 - 0
AUTHORS

@@ -57,3 +57,6 @@ Arseniy Shestakov aka SXX,      <[email protected]>
 
 Vadim Markovtsev, <[email protected]>
    * resolving problems with macOS, bug fixes
+
+Michał Kalinowski, <[email protected]>
+   * refactoring code