Browse Source

BattleAI: spellcast fixes and floating point score

Andrii Danylchenko 2 years ago
parent
commit
5f13a0bbda

+ 8 - 7
AI/BattleAI/AttackPossibility.cpp

@@ -22,7 +22,7 @@ void DamageCache::cacheDamage(const battle::Unit * attacker, const battle::Unit
 {
 	auto damage = averageDmg(hb->battleEstimateDamage(attacker, defender, 0).damage);
 
-	damageCache[attacker->unitId()][defender->unitId()] = damage / attacker->getCount();
+	damageCache[attacker->unitId()][defender->unitId()] = static_cast<float>(damage) / attacker->getCount();
 }
 
 
@@ -98,18 +98,18 @@ AttackPossibility::AttackPossibility(BattleHex from, BattleHex dest, const Battl
 {
 }
 
-int64_t AttackPossibility::damageDiff() const
+float AttackPossibility::damageDiff() const
 {
 	return defenderDamageReduce - attackerDamageReduce - collateralDamageReduce + shootersBlockedDmg;
 }
 
-int64_t AttackPossibility::damageDiff(float positiveEffectMultiplier, float negativeEffectMultiplier) const
+float AttackPossibility::damageDiff(float positiveEffectMultiplier, float negativeEffectMultiplier) const
 {
 	return positiveEffectMultiplier * (defenderDamageReduce + shootersBlockedDmg)
 		- negativeEffectMultiplier * (attackerDamageReduce + collateralDamageReduce);
 }
 
-int64_t AttackPossibility::attackValue() const
+float AttackPossibility::attackValue() const
 {
 	return damageDiff();
 }
@@ -119,7 +119,7 @@ int64_t AttackPossibility::attackValue() const
 /// Half bounty for kill, half for making damage equal to enemy health
 /// Bounty - the killed creature average damage calculated against attacker
 /// </summary>
-int64_t AttackPossibility::calculateDamageReduce(
+float AttackPossibility::calculateDamageReduce(
 	const battle::Unit * attacker,
 	const battle::Unit * defender,
 	uint64_t damageDealt,
@@ -163,7 +163,7 @@ int64_t AttackPossibility::calculateDamageReduce(
 	auto firstUnitHealthRatio = firstUnitHpLeft == 0 ? 1 : static_cast<float>(firstUnitHpLeft) / maxHealth;
 	auto firstUnitKillValue = (1 - firstUnitHealthRatio) * (1 - firstUnitHealthRatio);
 
-	return (int64_t)(damagePerEnemy * (enemiesKilled + firstUnitKillValue * HEALTH_BOUNTY));
+	return damagePerEnemy * (enemiesKilled + firstUnitKillValue * HEALTH_BOUNTY);
 }
 
 int64_t AttackPossibility::evaluateBlockedShootersDmg(
@@ -270,7 +270,8 @@ AttackPossibility AttackPossibility::evaluate(
 
 			for(int i = 0; i < totalAttacks; i++)
 			{
-				int64_t damageDealt, damageReceived, defenderDamageReduce, attackerDamageReduce;
+				int64_t damageDealt, damageReceived;
+				float defenderDamageReduce, attackerDamageReduce;
 
 				DamageEstimation retaliation;
 				auto attackDmg = state->battleEstimateDamage(ap.attack, &retaliation);

+ 7 - 7
AI/BattleAI/AttackPossibility.h

@@ -46,16 +46,16 @@ public:
 
 	std::vector<std::shared_ptr<battle::CUnitState>> affectedUnits;
 
-	int64_t defenderDamageReduce = 0;
-	int64_t attackerDamageReduce = 0; //usually by counter-attack
-	int64_t collateralDamageReduce = 0; // friendly fire (usually by two-hex attacks)
+	float defenderDamageReduce = 0;
+	float attackerDamageReduce = 0; //usually by counter-attack
+	float collateralDamageReduce = 0; // friendly fire (usually by two-hex attacks)
 	int64_t shootersBlockedDmg = 0;
 
 	AttackPossibility(BattleHex from, BattleHex dest, const BattleAttackInfo & attack_);
 
-	int64_t damageDiff() const;
-	int64_t attackValue() const;
-	int64_t damageDiff(float positiveEffectMultiplier, float negativeEffectMultiplier) const;
+	float damageDiff() const;
+	float attackValue() const;
+	float damageDiff(float positiveEffectMultiplier, float negativeEffectMultiplier) const;
 
 	static AttackPossibility evaluate(
 		const BattleAttackInfo & attackInfo,
@@ -63,7 +63,7 @@ public:
 		DamageCache & damageCache,
 		std::shared_ptr<CBattleInfoCallback> state);
 
-	static int64_t calculateDamageReduce(
+	static float calculateDamageReduce(
 		const battle::Unit * attacker,
 		const battle::Unit * defender,
 		uint64_t damageDealt,

+ 27 - 6
AI/BattleAI/BattleAI.cpp

@@ -81,7 +81,28 @@ void CBattleAI::yourTacticPhase(int distance)
 	cb->battleMakeTacticAction(BattleAction::makeEndOFTacticPhase(cb->battleGetTacticsSide()));
 }
 
-void CBattleAI::activeStack( const CStack * stack )
+float getStrengthRatio(std::shared_ptr<CBattleCallback> cb, int side)
+{
+	auto stacks = cb->battleGetAllStacks();
+	auto our = 0, enemy = 0;
+
+	for(auto stack : stacks)
+	{
+		auto creature = stack->creatureId().toCreature();
+
+		if(!creature)
+			continue;
+
+		if(stack->unitSide() == side)
+			our += stack->getCount() * creature->getAIValue();
+		else
+			enemy += stack->getCount() * creature->getAIValue();
+	}
+
+	return enemy == 0 ? 1.0f : static_cast<float>(our) / enemy;
+}
+
+void CBattleAI::activeStack(const CStack * stack )
 {
 	LOG_TRACE_PARAMS(logAi, "stack: %s", stack->nodeName());
 
@@ -110,7 +131,11 @@ void CBattleAI::activeStack( const CStack * stack )
 			return;
 		}
 
-		BattleEvaluator evaluator(env, cb, stack, playerID, side, strengthRatio);
+#if BATTLE_TRACE_LEVEL>=1
+		logAi->trace("Build evaluator and targets");
+#endif
+
+		BattleEvaluator evaluator(env, cb, stack, playerID, side, getStrengthRatio(cb, side));
 
 		result = evaluator.selectStackAction(stack);
 
@@ -207,10 +232,6 @@ void CBattleAI::battleStart(const CCreatureSet *army1, const CCreatureSet *army2
 {
 	LOG_TRACE(logAi);
 	side = Side;
-	strengthRatio = static_cast<float>(army1->getArmyStrength()) / static_cast<float>(army2->getArmyStrength());
-
-	if(side == 1)
-		strengthRatio = 1 / strengthRatio;
 
 	skipCastUntilNextBattle = false;
 }

+ 0 - 1
AI/BattleAI/BattleAI.h

@@ -62,7 +62,6 @@ class CBattleAI : public CBattleGameInterface
 	bool wasWaitingForRealize;
 	bool wasUnlockingGs;
 	int movesSkippedByDefense;
-	float strengthRatio;
 	bool skipCastUntilNextBattle;
 
 public:

+ 37 - 6
AI/BattleAI/BattleEvaluator.cpp

@@ -100,11 +100,14 @@ std::optional<PossibleSpellcast> BattleEvaluator::findBestCreatureSpell(const CS
 
 BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 {
+#if BATTLE_TRACE_LEVEL >= 1
+	logAi->trace("Select stack action");
+#endif
 	//evaluate casting spell for spellcasting stack
 	std::optional<PossibleSpellcast> bestSpellcast = findBestCreatureSpell(stack);
 
 	auto moveTarget = scoreEvaluator.findMoveTowardsUnreachable(stack, *targets, damageCache, hb);
-	auto score = EvaluationResult::INEFFECTIVE_SCORE;
+	float score = EvaluationResult::INEFFECTIVE_SCORE;
 
 	if(targets->possibleAttacks.empty() && bestSpellcast.has_value())
 	{
@@ -136,7 +139,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 		{
 			score = evaluationResult.score;
 
-			logAi->debug("BattleAI: %s -> %s x %d, from %d curpos %d dist %d speed %d: +%lld -%lld = %lld",
+			logAi->debug("BattleAI: %s -> %s x %d, from %d curpos %d dist %d speed %d: +%2f -%2f = %2f",
 				bestAttack.attackerState->unitType()->getJsonKey(),
 				bestAttack.affectedUnits[0]->unitType()->getJsonKey(),
 				(int)bestAttack.affectedUnits[0]->getCount(),
@@ -145,7 +148,8 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 				bestAttack.attack.chargeDistance,
 				bestAttack.attack.attacker->speed(0, true),
 				bestAttack.defenderDamageReduce,
-				bestAttack.attackerDamageReduce, bestAttack.attackValue()
+				bestAttack.attackerDamageReduce,
+				bestAttack.attackValue()
 			);
 
 			if (moveTarget.scorePerTurn <= score)
@@ -513,11 +517,20 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 
 	CStopWatch timer;
 
+#if BATTLE_TRACE_LEVEL >= 1
+	tbb::blocked_range<size_t> r(0, possibleCasts.size());
+#else
 	tbb::parallel_for(tbb::blocked_range<size_t>(0, possibleCasts.size()), [&](const tbb::blocked_range<size_t> & r)
 		{
+#endif
 			for(auto i = r.begin(); i != r.end(); i++)
 			{
 				auto & ps = possibleCasts[i];
+
+#if BATTLE_TRACE_LEVEL >= 1
+				logAi->trace("Evaluating %s", ps.spell->getNameTranslated());
+#endif
+
 				auto state = std::make_shared<HypotheticBattle>(env.get(), cb);
 
 				spells::BattleCast cast(state.get(), hero, spells::Mode::HERO, ps.spell);
@@ -531,12 +544,17 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 						return  !original || u->speed() != original->speed();
 					});
 
-				DamageCache innerCache(&damageCache);
+				DamageCache safeCopy = damageCache;
+				DamageCache innerCache(&safeCopy);
 				innerCache.buildDamageCache(state, side);
 
 				if(needFullEval || !cachedAttack)
 				{
-					PotentialTargets innerTargets(activeStack, damageCache, state);
+#if BATTLE_TRACE_LEVEL >= 1
+					logAi->trace("Full evaluation is started due to stack speed affected.");
+#endif
+
+					PotentialTargets innerTargets(activeStack, innerCache, state);
 					BattleExchangeEvaluator innerEvaluator(state, env, strengthRatio);
 
 					if(!innerTargets.possibleAttacks.empty())
@@ -586,14 +604,27 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 						}
 						else
 							ps.value -= dpsReduce * scoreEvaluator.getNegativeEffectMultiplier();
+
+#if BATTLE_TRACE_LEVEL >= 1
+						logAi->trace(
+							"Spell affects %s (%d), dps: %2f",
+							unit->creatureId().toCreature()->getNameSingularTranslated(),
+							unit->getCount(),
+							dpsReduce);
+#endif
 					}
 				}
+#if BATTLE_TRACE_LEVEL >= 1
+				logAi->trace("Total score: %2f", ps.value);
+#endif
 			}
+#if BATTLE_TRACE_LEVEL == 0
 		});
+#endif
 
 	LOGFL("Evaluation took %d ms", timer.getDiff());
 
-	auto pscValue = [](const PossibleSpellcast &ps) -> int64_t
+	auto pscValue = [](const PossibleSpellcast &ps) -> float
 	{
 		return ps.value;
 	};

+ 1 - 1
AI/BattleAI/BattleEvaluator.h

@@ -33,7 +33,7 @@ class BattleEvaluator
 	std::optional<AttackPossibility> cachedAttack;
 	PlayerColor playerID;
 	int side;
-	int64_t cachedScore;
+	float cachedScore;
 	DamageCache damageCache;
 	float strengthRatio;
 

+ 141 - 77
AI/BattleAI/BattleExchangeVariant.cpp

@@ -25,40 +25,107 @@ MoveTarget::MoveTarget()
 	turnsToRich = 1;
 }
 
-int64_t BattleExchangeVariant::trackAttack(const AttackPossibility & ap, HypotheticBattle & state)
+float BattleExchangeVariant::trackAttack(
+	const AttackPossibility & ap,
+	std::shared_ptr<HypotheticBattle> hb,
+	DamageCache & damageCache)
 {
+	auto attacker = hb->getForUpdate(ap.attack.attacker->unitId());
+
+	const std::string cachingStringBlocksRetaliation = "type_BLOCKS_RETALIATION";
+	static const auto selectorBlocksRetaliation = Selector::type()(BonusType::BLOCKS_RETALIATION);
+	const bool counterAttacksBlocked = attacker->hasBonus(selectorBlocksRetaliation, cachingStringBlocksRetaliation);
+
+	float attackValue = 0;
 	auto affectedUnits = ap.affectedUnits;
 
 	affectedUnits.push_back(ap.attackerState);
 
 	for(auto affectedUnit : affectedUnits)
 	{
-		auto unitToUpdate = state.getForUpdate(affectedUnit->unitId());
+		auto unitToUpdate = hb->getForUpdate(affectedUnit->unitId());
 
-		unitToUpdate->health = affectedUnit->health;
-		unitToUpdate->shots = affectedUnit->shots;
-		unitToUpdate->counterAttacks = affectedUnit->counterAttacks;
-		unitToUpdate->movedThisRound = affectedUnit->movedThisRound;
-	}
+		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 -= attackerDamageReduce * negativeEffectMultiplier;
+				attackerValue[unitToUpdate->unitId()].isRetalitated = true;
+
+				unitToUpdate->damage(retaliationDamage);
+				defender->afterAttack(false, true);
+
+#if BATTLE_TRACE_LEVEL>=1
+				logAi->trace(
+					"%s -> %s, ap retalitation, %s, dps: %2f, score: %2f",
+					defender->getDescription(),
+					unitToUpdate->getDescription(),
+					ap.attack.shooting ? "shot" : "mellee",
+					retaliationDamage,
+					attackerDamageReduce);
+#endif
+			}
+			else
+			{
+				auto collateralDamage = damageCache.getDamage(attacker.get(), unitToUpdate.get(), hb);
+				auto collateralDamageReduce = AttackPossibility::calculateDamageReduce(attacker.get(), unitToUpdate.get(), collateralDamage, damageCache, hb);
 
-	auto attackValue = ap.damageDiff(positiveEffectMultiplier, negativeEffectMultiplier);
+				attackValue -= collateralDamageReduce;
+				dpsScore -= collateralDamageReduce * negativeEffectMultiplier;
 
-	dpsScore += attackValue;
+				unitToUpdate->damage(collateralDamage);
 
 #if BATTLE_TRACE_LEVEL>=1
-	logAi->trace(
-		"%s -> %s, ap attack, %s, dps: %lld, score: %lld",
-		ap.attack.attacker->getDescription(),
-		ap.attack.defender->getDescription(),
-		ap.attack.shooting ? "shot" : "mellee",
-		ap.damageDealt,
-		attackValue);
+				logAi->trace(
+					"%s -> %s, ap collateral, %s, dps: %2f, score: %2f",
+					attacker->getDescription(),
+					unitToUpdate->getDescription(),
+					ap.attack.shooting ? "shot" : "mellee",
+					collateralDamage,
+					collateralDamageReduce);
 #endif
+			}
+		}
+		else
+		{
+			int64_t attackDamage = damageCache.getDamage(attacker.get(), unitToUpdate.get(), hb);
+			float defenderDamageReduce = AttackPossibility::calculateDamageReduce(attacker.get(), unitToUpdate.get(), attackDamage, damageCache, hb);
+
+			attackValue += defenderDamageReduce;
+			dpsScore += defenderDamageReduce * positiveEffectMultiplier;
+			attackerValue[attacker->unitId()].value += defenderDamageReduce;
+
+			unitToUpdate->damage(attackDamage);
+
+#if BATTLE_TRACE_LEVEL>=1
+			logAi->trace(
+				"%s -> %s, ap attack, %s, dps: %2f, score: %2f",
+				attacker->getDescription(),
+				unitToUpdate->getDescription(),
+				ap.attack.shooting ? "shot" : "mellee",
+				attackDamage,
+				defenderDamageReduce);
+#endif
+		}
+	}
+
+	attackValue += ap.shootersBlockedDmg;
+	dpsScore += ap.shootersBlockedDmg * positiveEffectMultiplier;
+	attacker->afterAttack(ap.attack.shooting, false);
 
 	return attackValue;
 }
 
-int64_t BattleExchangeVariant::trackAttack(
+float BattleExchangeVariant::trackAttack(
 	std::shared_ptr<StackWithBonuses> attacker,
 	std::shared_ptr<StackWithBonuses> defender,
 	bool shooting,
@@ -71,23 +138,15 @@ int64_t BattleExchangeVariant::trackAttack(
 	static const auto selectorBlocksRetaliation = Selector::type()(BonusType::BLOCKS_RETALIATION);
 	const bool counterAttacksBlocked = attacker->hasBonus(selectorBlocksRetaliation, cachingStringBlocksRetaliation);
 
-	// FIXME: provide distance info for Jousting bonus
-	BattleAttackInfo bai(attacker.get(), defender.get(), 0, shooting);
-
-	if(shooting)
-	{
-		bai.attackerPos.setXY(8, 5);
-	}
-
 	int64_t attackDamage = damageCache.getDamage(attacker.get(), defender.get(), hb);
-	int64_t defenderDamageReduce = AttackPossibility::calculateDamageReduce(attacker.get(), defender.get(), attackDamage, damageCache, hb);
-	int64_t attackerDamageReduce = 0;
+	float defenderDamageReduce = AttackPossibility::calculateDamageReduce(attacker.get(), defender.get(), attackDamage, damageCache, hb);
+	float attackerDamageReduce = 0;
 
 	if(!evaluateOnly)
 	{
 #if BATTLE_TRACE_LEVEL>=1
 		logAi->trace(
-			"%s -> %s, normal attack, %s, dps: %lld, %lld",
+			"%s -> %s, normal attack, %s, dps: %lld, %2f",
 			attacker->getDescription(),
 			defender->getDescription(),
 			shooting ? "shot" : "mellee",
@@ -107,36 +166,33 @@ int64_t BattleExchangeVariant::trackAttack(
 		attacker->afterAttack(shooting, false);
 	}
 
-	if(defender->alive() && defender->ableToRetaliate() && !counterAttacksBlocked && !shooting)
+	if(!evaluateOnly && defender->alive() && defender->ableToRetaliate() && !counterAttacksBlocked && !shooting)
 	{
 		auto retaliationDamage = damageCache.getDamage(defender.get(), attacker.get(), hb);
 		attackerDamageReduce = AttackPossibility::calculateDamageReduce(defender.get(), attacker.get(), retaliationDamage, damageCache, hb);
 
-		if(!evaluateOnly)
-		{
 #if BATTLE_TRACE_LEVEL>=1
-			logAi->trace(
-				"%s -> %s, retaliation, dps: %lld, %lld",
-				defender->getDescription(),
-				attacker->getDescription(),
-				retaliationDamage,
-				attackerDamageReduce);
+		logAi->trace(
+			"%s -> %s, retaliation, dps: %lld, %2f",
+			defender->getDescription(),
+			attacker->getDescription(),
+			retaliationDamage,
+			attackerDamageReduce);
 #endif
 
-			if(isOurAttack)
-			{
-				dpsScore -= attackerDamageReduce * negativeEffectMultiplier;
-				attackerValue[attacker->unitId()].isRetalitated = true;
-			}
-			else
-			{
-				dpsScore += attackerDamageReduce * positiveEffectMultiplier;
-				attackerValue[defender->unitId()].value += attackerDamageReduce;
-			}
-
-			attacker->damage(retaliationDamage);
-			defender->afterAttack(false, true);
+		if(isOurAttack)
+		{
+			dpsScore -= attackerDamageReduce * negativeEffectMultiplier;
+			attackerValue[attacker->unitId()].isRetalitated = true;
+		}
+		else
+		{
+			dpsScore += attackerDamageReduce * positiveEffectMultiplier;
+			attackerValue[defender->unitId()].value += attackerDamageReduce;
 		}
+
+		attacker->damage(retaliationDamage);
+		defender->afterAttack(false, true);
 	}
 
 	auto score = defenderDamageReduce - attackerDamageReduce;
@@ -144,7 +200,7 @@ int64_t BattleExchangeVariant::trackAttack(
 #if BATTLE_TRACE_LEVEL>=1
 	if(!score)
 	{
-		logAi->trace("Attack has zero score d:%lld a:%lld", defenderDamageReduce, attackerDamageReduce);
+		logAi->trace("Attack has zero score d:%2f a:%2f", defenderDamageReduce, attackerDamageReduce);
 	}
 #endif
 
@@ -159,33 +215,22 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
 {
 	EvaluationResult result(targets.bestAction());
 
-	updateReachabilityMap(hb);
-
-	for(auto & ap : targets.possibleAttacks)
-	{
-		int64_t score = calculateExchange(ap, targets, damageCache, hb);
-
-		if(score > result.score)
-		{
-			result.score = score;
-			result.bestAttack = ap;
-		}
-	}
-
 	if(!activeStack->waited())
 	{
 #if BATTLE_TRACE_LEVEL>=1
 		logAi->trace("Evaluating waited attack for %s", activeStack->getDescription());
 #endif
 
-		hb->getForUpdate(activeStack->unitId())->waiting = true;
-		hb->getForUpdate(activeStack->unitId())->waitedThisTurn = true;
+		auto hbWaited = std::make_shared<HypotheticBattle>(env.get(), hb);
 
-		updateReachabilityMap(hb);
+		hbWaited->getForUpdate(activeStack->unitId())->waiting = true;
+		hbWaited->getForUpdate(activeStack->unitId())->waitedThisTurn = true;
+
+		updateReachabilityMap(hbWaited);
 
 		for(auto & ap : targets.possibleAttacks)
 		{
-			int64_t score = calculateExchange(ap, targets, damageCache, hb);
+			float score = calculateExchange(ap, targets, damageCache, hbWaited);
 
 			if(score > result.score)
 			{
@@ -196,6 +241,24 @@ EvaluationResult BattleExchangeEvaluator::findBestTarget(
 		}
 	}
 
+#if BATTLE_TRACE_LEVEL>=1
+	logAi->trace("Evaluating normal attack for %s", activeStack->getDescription());
+#endif
+
+	updateReachabilityMap(hb);
+
+	for(auto & ap : targets.possibleAttacks)
+	{
+		float score = calculateExchange(ap, targets, damageCache, hb);
+
+		if(score >= result.score)
+		{
+			result.score = score;
+			result.bestAttack = ap;
+			result.wait = false;
+		}
+	}
+
 	return result;
 }
 
@@ -361,14 +424,14 @@ std::vector<const battle::Unit *> BattleExchangeEvaluator::getExchangeUnits(
 	return exchangeUnits;
 }
 
-int64_t BattleExchangeEvaluator::calculateExchange(
+float BattleExchangeEvaluator::calculateExchange(
 	const AttackPossibility & ap,
 	PotentialTargets & targets,
 	DamageCache & damageCache,
 	std::shared_ptr<HypotheticBattle> hb)
 {
 #if BATTLE_TRACE_LEVEL>=1
-	logAi->trace("Battle exchange at %lld", ap.attack.shooting ? ap.dest : ap.from);
+	logAi->trace("Battle exchange at %d", ap.attack.shooting ? ap.dest.hex : ap.from.hex);
 #endif
 
 	if(cb->battleGetMySide() == BattlePerspective::LEFT_SIDE
@@ -439,7 +502,7 @@ int64_t BattleExchangeEvaluator::calculateExchange(
 
 		if(!isOur || !exchangeBattle->battleGetUnitByID(targetUnit->unitId())->alive())
 		{
-			auto estimateAttack = [&](const battle::Unit * u) -> int64_t
+			auto estimateAttack = [&](const battle::Unit * u) -> float
 			{
 				auto stackWithBonuses = exchangeBattle->getForUpdate(u->unitId());
 				auto score = v.trackAttack(
@@ -452,7 +515,7 @@ int64_t BattleExchangeEvaluator::calculateExchange(
 					true);
 
 #if BATTLE_TRACE_LEVEL>=1
-				logAi->trace("Best target selector %s->%s score = %lld", attacker->getDescription(), u->getDescription(), score);
+				logAi->trace("Best target selector %s->%s score = %2f", attacker->getDescription(), u->getDescription(), score);
 #endif
 
 				return score;
@@ -497,9 +560,10 @@ int64_t BattleExchangeEvaluator::calculateExchange(
 		auto shooting = exchangeBattle->battleCanShoot(attacker.get());
 		const int totalAttacks = attacker->getTotalAttacks(shooting);
 
-		if(canUseAp && activeUnit->unitId() == ap.attack.attacker->unitId() && targetUnit->unitId() == ap.attack.defender->unitId())
+		if(canUseAp && activeUnit->unitId() == ap.attack.attacker->unitId()
+			&& targetUnit->unitId() == ap.attack.defender->unitId())
 		{
-			v.trackAttack(ap, *exchangeBattle);
+			v.trackAttack(ap, exchangeBattle, damageCache);
 		}
 		else
 		{
@@ -530,7 +594,7 @@ int64_t BattleExchangeEvaluator::calculateExchange(
 	v.adjustPositions(melleeAttackers, ap, reachabilityMap);
 
 #if BATTLE_TRACE_LEVEL>=1
-	logAi->trace("Exchange score: %lld", v.getScore());
+	logAi->trace("Exchange score: %2f", v.getScore());
 #endif
 
 	return v.getScore();
@@ -560,7 +624,7 @@ void BattleExchangeVariant::adjustPositions(
 		vstd::erase_if_present(hexes, ap.attack.attacker->occupiedHex(ap.attack.attackerPos));
 	}
 
-	int64_t notRealizedDamage = 0;
+	float notRealizedDamage = 0;
 
 	for(auto unit : attackers)
 	{
@@ -576,7 +640,7 @@ void BattleExchangeVariant::adjustPositions(
 			continue;
 		}
 
-		auto desiredPosition = vstd::minElementByFun(hexes, [&](BattleHex h) -> int64_t
+		auto desiredPosition = vstd::minElementByFun(hexes, [&](BattleHex h) -> float
 			{
 				auto score = vstd::contains(reachabilityMap[h], unit)
 					? reachabilityMap[h].size()

+ 13 - 10
AI/BattleAI/BattleExchangeVariant.h

@@ -16,7 +16,7 @@
 
 struct AttackerValue
 {
-	int64_t value;
+	float value;
 	bool isRetalitated;
 	BattleHex position;
 
@@ -25,8 +25,8 @@ struct AttackerValue
 
 struct MoveTarget
 {
-	int64_t score;
-	int64_t scorePerTurn;
+	float score;
+	float scorePerTurn;
 	std::vector<BattleHex> positions;
 	std::optional<AttackPossibility> cachedAttack;
 	uint8_t turnsToRich;
@@ -36,12 +36,12 @@ struct MoveTarget
 
 struct EvaluationResult
 {
-	static const int64_t INEFFECTIVE_SCORE = -1000000;
+	static const int64_t INEFFECTIVE_SCORE = -10000;
 
 	AttackPossibility bestAttack;
 	MoveTarget bestMove;
 	bool wait;
-	int64_t score;
+	float score;
 	bool defend;
 
 	EvaluationResult(const AttackPossibility & ap)
@@ -62,9 +62,12 @@ public:
 	BattleExchangeVariant(float positiveEffectMultiplier, float negativeEffectMultiplier)
 		: dpsScore(0), positiveEffectMultiplier(positiveEffectMultiplier), negativeEffectMultiplier(negativeEffectMultiplier) {}
 
-	int64_t trackAttack(const AttackPossibility & ap, HypotheticBattle & state);
+	float trackAttack(
+		const AttackPossibility & ap,
+		std::shared_ptr<HypotheticBattle> hb,
+		DamageCache & damageCache);
 
-	int64_t trackAttack(
+	float trackAttack(
 		std::shared_ptr<StackWithBonuses> attacker,
 		std::shared_ptr<StackWithBonuses> defender,
 		bool shooting,
@@ -73,7 +76,7 @@ public:
 		std::shared_ptr<HypotheticBattle> hb,
 		bool evaluateOnly = false);
 
-	int64_t getScore() const { return dpsScore; }
+	float getScore() const { return dpsScore; }
 
 	void adjustPositions(
 		std::vector<const battle::Unit *> attackers,
@@ -83,7 +86,7 @@ public:
 private:
 	float positiveEffectMultiplier;
 	float negativeEffectMultiplier;
-	int64_t dpsScore;
+	float dpsScore;
 	std::map<uint32_t, AttackerValue> attackerValue;
 };
 
@@ -110,7 +113,7 @@ public:
 		DamageCache & damageCache,
 		std::shared_ptr<HypotheticBattle> hb);
 
-	int64_t calculateExchange(
+	float calculateExchange(
 		const AttackPossibility & ap,
 		PotentialTargets & targets,
 		DamageCache & damageCache,

+ 1 - 1
AI/BattleAI/PossibleSpellcast.h

@@ -27,7 +27,7 @@ public:
 
 	const CSpell * spell;
 	spells::Target dest;
-	int64_t value;
+	float value;
 
 	PossibleSpellcast();
 	virtual ~PossibleSpellcast();

+ 6 - 3
AI/BattleAI/StackWithBonuses.cpp

@@ -45,7 +45,8 @@ StackWithBonuses::StackWithBonuses(const HypotheticBattle * Owner, const battle:
 	id(Stack->unitId()),
 	side(Stack->unitSide()),
 	player(Stack->unitOwner()),
-	slot(Stack->unitSlot())
+	slot(Stack->unitSlot()),
+	treeVersionLocal(0)
 {
 	localInit(Owner);
 
@@ -61,7 +62,8 @@ StackWithBonuses::StackWithBonuses(const HypotheticBattle * Owner, const battle:
 	id(Stack->unitId()),
 	side(Stack->unitSide()),
 	player(Stack->unitOwner()),
-	slot(Stack->unitSlot())
+	slot(Stack->unitSlot()),
+	treeVersionLocal(0)
 {
 	localInit(Owner);
 
@@ -76,7 +78,8 @@ StackWithBonuses::StackWithBonuses(const HypotheticBattle * Owner, const battle:
 	baseAmount(info.count),
 	id(info.id),
 	side(info.side),
-	slot(SlotID::SUMMONED_SLOT_PLACEHOLDER)
+	slot(SlotID::SUMMONED_SLOT_PLACEHOLDER),
+	treeVersionLocal(0)
 {
 	type = info.type.toCreature();
 	origBearer = type;