Browse Source

Merge pull request #4306 from vcmi/battle-ai-bait-archers

BattleAI: fix bait for archers when need to go long way
Andrii Danylchenko 1 year ago
parent
commit
9d18a2269c

+ 8 - 1
AI/BattleAI/AttackPossibility.cpp

@@ -201,6 +201,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)
@@ -208,9 +210,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;
@@ -220,9 +226,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;

+ 10 - 1
AI/BattleAI/BattleAI.cpp

@@ -23,6 +23,7 @@
 #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
@@ -122,6 +123,11 @@ static float getStrengthRatio(std::shared_ptr<CBattleInfoCallback> cb, BattleSid
 	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());
@@ -154,7 +160,10 @@ 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);
 

+ 101 - 44
AI/BattleAI/BattleEvaluator.cpp

@@ -49,6 +49,45 @@ 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);
+	cachedScore = EvaluationResult::INEFFECTIVE_SCORE;
+}
+
+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);
+	cachedScore = EvaluationResult::INEFFECTIVE_SCORE;
+}
+
 std::vector<BattleHex> BattleEvaluator::getBrokenWallMoatHexes() const
 {
 	std::vector<BattleHex> result;
@@ -167,7 +206,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 				score
 			);
 
-			if (moveTarget.scorePerTurn <= score)
+			if (moveTarget.score <= score)
 			{
 				if(evaluationResult.wait)
 				{
@@ -197,7 +236,7 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 	}
 
 	//ThreatMap threatsToUs(stack); // These lines may be useful but they are't used in the code.
-	if(moveTarget.scorePerTurn > score)
+	if(moveTarget.score > score)
 	{
 		score = moveTarget.score;
 		cachedAttack = moveTarget.cachedAttack;
@@ -206,14 +245,13 @@ BattleAction BattleEvaluator::selectStackAction(const CStack * stack)
 		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
 		{
@@ -235,7 +273,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,7 +287,32 @@ uint64_t timeElapsed(std::chrono::time_point<std::chrono::high_resolution_clock>
 	return std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
 }
 
-BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes)
+BattleAction BattleEvaluator::moveOrAttack(const CStack * stack, BattleHex hex, const PotentialTargets & targets)
+{
+	auto additionalScore = 0;
+	std::optional<AttackPossibility> attackOnTheWay;
+
+	for(auto & target : targets.possibleAttacks)
+	{
+		if(!target.attack.shooting && target.from == hex && target.attackValue() > additionalScore)
+		{
+			additionalScore = target.attackValue();
+			attackOnTheWay = target;
+		}
+	}
+
+	if(attackOnTheWay)
+	{
+		activeActionMade = true;
+		return BattleAction::makeMeleeAttack(stack, attackOnTheWay->attack.defender->getPosition(), attackOnTheWay->from);
+	}
+	else
+	{
+		return BattleAction::makeMove(stack, hex);
+	}
+}
+
+BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector<BattleHex> hexes, const PotentialTargets & targets)
 {
 	auto reachability = cb->getBattle(battleID)->getReachability(stack);
 	auto avHexes = cb->getBattle(battleID)->battleGetAvailableHexes(reachability, stack, false);
@@ -261,49 +324,38 @@ BattleAction BattleEvaluator::goTowardsNearest(const CStack * stack, std::vector
 
 	std::vector<BattleHex> targetHexes = hexes;
 
-	for(int i = 0; i < 5; i++)
-	{
-		std::sort(targetHexes.begin(), targetHexes.end(), [&](BattleHex h1, BattleHex h2) -> bool
-			{
-				return reachability.distances[h1] < reachability.distances[h2];
-			});
+	vstd::erase_if(targetHexes, [](const BattleHex & hex) { return !hex.isValid(); });
 
-		for(auto hex : targetHexes)
+	std::sort(targetHexes.begin(), targetHexes.end(), [&](BattleHex h1, BattleHex h2) -> bool
 		{
-			if(vstd::contains(avHexes, hex))
-			{
-				return BattleAction::makeMove(stack, hex);
-			}
-
-			if(stack->coversPos(hex))
-			{
-				logAi->warn("Warning: already standing on neighbouring tile!");
-				//We shouldn't even be here...
-				return BattleAction::makeDefend(stack);
-			}
-		}
-
-		if(reachability.distances[targetHexes.front()] <= GameConstants::BFIELD_SIZE)
-		{
-			break;
-		}
-
-		std::vector<BattleHex> copy = targetHexes;
-
-		for(auto hex : copy)
-			vstd::concatenate(targetHexes, hex.allNeighbouringTiles());
-
-		vstd::erase_if(targetHexes, [](const BattleHex & hex) {return !hex.isValid();});
-		vstd::removeDuplicates(targetHexes);
-	}
+			return reachability.distances[h1] < reachability.distances[h2];
+		});
 
 	BattleHex bestNeighbor = targetHexes.front();
 
 	if(reachability.distances[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))
@@ -343,7 +395,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 +409,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()
@@ -600,7 +657,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
 #endif
 
 					PotentialTargets innerTargets(activeStack, innerCache, state);
-					BattleExchangeEvaluator innerEvaluator(state, env, strengthRatio);
+					BattleExchangeEvaluator innerEvaluator(state, env, strengthRatio, simulationTurnsCount);
 
 					if(!innerTargets.possibleAttacks.empty())
 					{

+ 7 - 16
AI/BattleAI/BattleEvaluator.h

@@ -37,16 +37,18 @@ class BattleEvaluator
 	float cachedScore;
 	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;
 	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,
@@ -55,15 +57,8 @@ public:
 		PlayerColor playerID,
 		BattleID battleID,
 		BattleSide 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;
-	}
+		float strengthRatio,
+		int simulationTurnsCount);
 
 	BattleEvaluator(
 		std::shared_ptr<Environment> env,
@@ -74,10 +69,6 @@ public:
 		PlayerColor playerID,
 		BattleID battleID,
 		BattleSide 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;
-	}
+		float strengthRatio,
+		int simulationTurnsCount);
 };

+ 290 - 125
AI/BattleAI/BattleExchangeVariant.cpp

@@ -18,7 +18,7 @@ AttackerValue::AttackerValue()
 }
 
 MoveTarget::MoveTarget()
-	: positions(), cachedAttack(), score(EvaluationResult::INEFFECTIVE_SCORE), scorePerTurn(EvaluationResult::INEFFECTIVE_SCORE)
+	: positions(), cachedAttack(), score(EvaluationResult::INEFFECTIVE_SCORE)
 {
 	turnsToRich = 1;
 }
@@ -42,7 +42,7 @@ float BattleExchangeVariant::trackAttack(
 	for(auto affectedUnit : affectedUnits)
 	{
 		auto unitToUpdate = hb->getForUpdate(affectedUnit->unitId());
-		auto damageDealt = unitToUpdate->getTotalHealth() - affectedUnit->getTotalHealth();
+		auto damageDealt = unitToUpdate->getAvailableHealth() - affectedUnit->getAvailableHealth();
 
 		if(damageDealt > 0)
 		{
@@ -58,7 +58,7 @@ float BattleExchangeVariant::trackAttack(
 #if BATTLE_TRACE_LEVEL>=1
 				logAi->trace(
 					"%s -> %s, ap retaliation, %s, dps: %lld",
-					ap.attack.defender->getDescription(),
+					hb->getForUpdate(ap.attack.defender->unitId())->getDescription(),
 					ap.attack.attacker->getDescription(),
 					ap.attack.shooting ? "shot" : "mellee",
 					damageDealt);
@@ -277,6 +277,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() / dmg;
+
+			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,
@@ -286,6 +316,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;
 
@@ -296,17 +328,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;
@@ -315,30 +347,84 @@ MoveTarget BattleExchangeEvaluator::findMoveTowardsUnreachable(
 			continue;
 
 		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;
-
-		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(BattleHex enemyHex : enemy->getAttackableHexes(activeStack))
+				{
+					while(!flying && dists.distances[enemyHex] > speed)
+					{
+						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;
 			}
@@ -382,7 +468,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;
 
@@ -390,13 +477,26 @@ 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));
 	}
 
+	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;
@@ -432,7 +532,7 @@ ReachabilityData BattleExchangeEvaluator::getExchangeUnits(
 
 	for(auto unit : allReachableUnits)
 	{
-		auto accessible = !unit->canShoot();
+		auto accessible = !unit->canShoot() || vstd::contains(additionalUnits, unit);
 
 		if(!accessible)
 		{
@@ -456,14 +556,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;
 }
@@ -494,7 +594,8 @@ 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);
@@ -513,7 +614,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())
 	{
@@ -523,22 +624,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
+			}
 		}
 	}
 
@@ -552,122 +656,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();
+
+	for(int exchangeTurn = 0; exchangeTurn < simulationTurnsCount; exchangeTurn++)
+	{
+		bool isMovingTurm = exchangeTurn < turn;
+		int queueTurn = exchangeTurn >= exchangeUnits.units.size()
+			? exchangeUnits.units.size() - 1
+			: exchangeTurn;
 
-		if(!attacker->alive())
+		for(auto activeUnit : exchangeUnits.units.at(queueTurn))
 		{
+			bool isOur = exchangeBattle->battleMatchOwner(ap.attack.attacker, activeUnit, true);
+			battle::Units & attackerQueue = isOur ? ourStacks : enemyStacks;
+			battle::Units & oppositeQueue = isOur ? enemyStacks : ourStacks;
+
+			auto attacker = exchangeBattle->getForUpdate(activeUnit->unitId());
+			auto shooting = exchangeBattle->battleCanShoot(attacker.get())
+				&& !vstd::contains(blockedShooters, attacker->unitId());
+
+			if(!attacker->alive())
+			{
 #if BATTLE_TRACE_LEVEL>=1
-			logAi->trace(	"Attacker is dead");
+				logAi->trace("Attacker is dead");
 #endif
 
-			continue;
-		}
-
-		auto targetUnit = ap.attack.defender;
+				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;
-			};
+				continue;
+			}
 
-			auto unitsInOppositeQueueExceptInaccessible = oppositeQueue;
+			auto targetUnit = ap.attack.defender;
 
-			vstd::erase_if(unitsInOppositeQueueExceptInaccessible, [&](const battle::Unit * u)->bool
-				{
-					return vstd::contains(exchangeUnits.shooters, u);
-				});
-
-			if(!unitsInOppositeQueueExceptInaccessible.empty())
+			if(!isOur || !exchangeBattle->battleGetUnitByID(targetUnit->unitId())->alive())
 			{
-				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 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()))
+				{
+					vstd::erase_if(unitsInOppositeQueueExceptInaccessible, [&](const battle::Unit * u) -> bool
+						{
+							return u->unitId() == ap.attack.attacker->unitId();
+						});
+				}
+
+				if(!unitsInOppositeQueueExceptInaccessible.empty())
 				{
-					targetUnit = *vstd::maxElementByFun(reachable, estimateAttack);
+					targetUnit = *vstd::maxElementByFun(unitsInOppositeQueueExceptInaccessible, estimateAttack);
 				}
 				else
 				{
+					auto reachable = exchangeBattle->battleGetUnitsIf([this, &exchangeBattle, &attacker](const battle::Unit * u) -> bool
+						{
+							if(u->unitSide() == attacker->unitSide())
+								return false;
+
+							if(!exchangeBattle->getForUpdate(u->unitId())->alive())
+								return false;
+
+							if(!u->getPosition().isValid())
+								return false; // e.g. tower shooters
+
+							return vstd::contains_if(reachabilityMap.at(u->getPosition()), [&attacker](const battle::Unit * other) -> bool
+								{
+									return attacker->unitId() == other->unitId();
+								});
+						});
+
+					if(!reachable.empty())
+					{
+						targetUnit = *vstd::maxElementByFun(reachable, estimateAttack);
+					}
+					else
+					{
 #if BATTLE_TRACE_LEVEL>=1
-					logAi->trace("Battle queue is empty and no reachable enemy.");
+						logAi->trace("Battle queue is empty and no reachable enemy.");
 #endif
 
-					continue;
+						continue;
+					}
 				}
 			}
-		}
 
-		auto defender = exchangeBattle->getForUpdate(targetUnit->unitId());
-		auto shooting = exchangeBattle->battleCanShoot(attacker.get());
-		const int totalAttacks = attacker->getTotalAttacks(shooting);
+			auto defender = exchangeBattle->getForUpdate(targetUnit->unitId());
+			const int totalAttacks = attacker->getTotalAttacks(shooting);
 
-		if(canUseAp && activeUnit->unitId() == ap.attack.attacker->unitId()
-			&& targetUnit->unitId() == ap.attack.defender->unitId())
-		{
-			v.trackAttack(ap, exchangeBattle, damageCache);
-		}
-		else
-		{
-			for(int i = 0; i < totalAttacks; i++)
+			if(canUseAp && activeUnit->unitId() == ap.attack.attacker->unitId()
+				&& targetUnit->unitId() == ap.attack.defender->unitId())
 			{
-				v.trackAttack(attacker, defender, shooting, isOur, damageCache, exchangeBattle);
+				v.trackAttack(ap, exchangeBattle, damageCache);
+			}
+			else
+			{
+				for(int i = 0; i < totalAttacks; i++)
+				{
+					v.trackAttack(attacker, defender, shooting, isOur, damageCache, exchangeBattle);
 
-				if(!attacker->alive() || !defender->alive())
-					break;
+					if(!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
@@ -679,11 +827,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)

+ 11 - 6
AI/BattleAI/BattleExchangeVariant.h

@@ -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,7 +150,8 @@ public:
 	BattleExchangeEvaluator(
 		std::shared_ptr<CBattleInfoCallback> cb,
 		std::shared_ptr<Environment> env,
-		float strengthRatio): cb(cb), env(env) {
+		float strengthRatio,
+		int simulationTurnsCount): cb(cb), env(env), simulationTurnsCount(simulationTurnsCount){
 		negativeEffectMultiplier = strengthRatio >= 1 ? 1 : strengthRatio * strengthRatio;
 	}
 
@@ -171,7 +175,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);
 

+ 8 - 0
AI/Nullkiller/Analyzers/DangerHitMapAnalyzer.cpp

@@ -89,6 +89,14 @@ 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();

+ 2 - 2
lib/CGameInfoCallback.h

@@ -56,7 +56,7 @@ public:
 
 //	//various
 	virtual int getDate(Date mode=Date::DAY) const = 0; //mode=0 - total days in game, mode=1 - day of week, mode=2 - current week, mode=3 - current month
-//	const StartInfo * getStartInfo(bool beforeRandomization = false)const;
+	virtual const StartInfo * getStartInfo(bool beforeRandomization = false) const = 0;
 	virtual bool isAllowed(SpellID id) const = 0;
 	virtual bool isAllowed(ArtifactID id) const = 0;
 	virtual bool isAllowed(SecondarySkill id) const = 0;
@@ -143,7 +143,7 @@ protected:
 public:
 	//various
 	int getDate(Date mode=Date::DAY)const override; //mode=0 - total days in game, mode=1 - day of week, mode=2 - current week, mode=3 - current month
-	virtual const StartInfo * getStartInfo(bool beforeRandomization = false)const;
+	const StartInfo * getStartInfo(bool beforeRandomization = false) const override;
 	bool isAllowed(SpellID id) const override;
 	bool isAllowed(ArtifactID id) const override;
 	bool isAllowed(SecondarySkill id) const override;

+ 12 - 2
lib/battle/AccessibilityInfo.cpp

@@ -18,9 +18,19 @@ VCMI_LIB_NAMESPACE_BEGIN
 bool AccessibilityInfo::tileAccessibleWithGate(BattleHex tile, BattleSide side) const
 {
 	//at(otherHex) != EAccessibility::ACCESSIBLE && (at(otherHex) != EAccessibility::GATE || side != BattleSide::DEFENDER)
-	if(at(tile) != EAccessibility::ACCESSIBLE)
-		if(at(tile) != EAccessibility::GATE || side != BattleSide::DEFENDER)
+	auto accessibility = at(tile);
+
+	if(accessibility == EAccessibility::ALIVE_STACK)
+	{
+		auto destructible = destructibleEnemyTurns.find(tile);
+
+		return destructible != destructibleEnemyTurns.end();
+	}
+
+	if(accessibility != EAccessibility::ACCESSIBLE)
+		if(accessibility != EAccessibility::GATE || side != BattleSide::DEFENDER)
 			return false;
+
 	return true;
 }
 

+ 2 - 0
lib/battle/AccessibilityInfo.h

@@ -35,6 +35,8 @@ using TAccessibilityArray = std::array<EAccessibility, GameConstants::BFIELD_SIZ
 
 struct DLL_LINKAGE AccessibilityInfo : TAccessibilityArray
 {
+	std::map<BattleHex, ui8> destructibleEnemyTurns;
+
 	public:
 		bool accessible(BattleHex tile, const battle::Unit * stack) const; //checks for both tiles if stack is double wide
 		bool accessible(BattleHex tile, bool doubleWide, BattleSide side) const; //checks for both tiles if stack is double wide

+ 22 - 3
lib/battle/CBattleInfoCallback.cpp

@@ -1052,16 +1052,29 @@ ReachabilityInfo CBattleInfoCallback::makeBFS(const AccessibilityInfo &accessibi
 			continue;
 
 		const int costToNeighbour = ret.distances[curHex.hex] + 1;
+
 		for(BattleHex neighbour : BattleHex::neighbouringTilesCache[curHex.hex])
 		{
 			if(neighbour.isValid())
 			{
+				auto additionalCost = 0;
+
+				if(params.bypassEnemyStacks)
+				{
+					auto enemyToBypass = params.destructibleEnemyTurns.find(neighbour);
+
+					if(enemyToBypass != params.destructibleEnemyTurns.end())
+					{
+						additionalCost = enemyToBypass->second;
+					}
+				}
+
 				const int costFoundSoFar = ret.distances[neighbour.hex];
 
-				if(accessibleCache[neighbour.hex] && costToNeighbour < costFoundSoFar)
+				if(accessibleCache[neighbour.hex] && costToNeighbour + additionalCost < costFoundSoFar)
 				{
 					hexq.push(neighbour);
-					ret.distances[neighbour.hex] = costToNeighbour;
+					ret.distances[neighbour.hex] = costToNeighbour + additionalCost;
 					ret.predecessors[neighbour.hex] = curHex;
 				}
 			}
@@ -1236,7 +1249,13 @@ ReachabilityInfo CBattleInfoCallback::getReachability(const ReachabilityInfo::Pa
 	if(params.flying)
 		return getFlyingReachability(params);
 	else
-		return makeBFS(getAccessibility(params.knownAccessible), params);
+	{
+		auto accessibility = getAccessibility(params.knownAccessible);
+
+		accessibility.destructibleEnemyTurns = params.destructibleEnemyTurns;
+
+		return makeBFS(accessibility, params);
+	}
 }
 
 ReachabilityInfo CBattleInfoCallback::getFlyingReachability(const ReachabilityInfo::Parameters &params) const

+ 2 - 0
lib/battle/ReachabilityInfo.h

@@ -29,7 +29,9 @@ struct DLL_LINKAGE ReachabilityInfo
 		bool doubleWide = false;
 		bool flying = false;
 		bool ignoreKnownAccessible = false; //Ignore obstacles if it is in accessible hexes
+		bool bypassEnemyStacks = false; // in case of true will count amount of turns needed to kill enemy and thus move forward
 		std::vector<BattleHex> knownAccessible; //hexes that will be treated as accessible, even if they're occupied by stack (by default - tiles occupied by stack we do reachability for, so it doesn't block itself)
+		std::map<BattleHex, ui8> destructibleEnemyTurns; // hom many turns it is needed to kill enemy on specific hex
 
 		BattleHex startPosition; //assumed position of stack
 		BattleSide perspective = BattleSide::ALL_KNOWING; //some obstacles (eg. quicksands) may be invisible for some side

+ 1 - 0
test/mock/mock_IGameInfoCallback.h

@@ -17,6 +17,7 @@ class IGameInfoCallbackMock : public IGameInfoCallback
 public:
 	//various
 	MOCK_CONST_METHOD1(getDate, int(Date));
+	MOCK_CONST_METHOD1(getStartInfo, const StartInfo *(bool));
 
 	MOCK_CONST_METHOD1(isAllowed, bool(SpellID));
 	MOCK_CONST_METHOD1(isAllowed, bool(ArtifactID));