Prechádzať zdrojové kódy

Further changes for duel mode. It is possible to pass names of AIs to be used by command line. Moved things in battle AI.

Michał W. Urbańczyk 12 rokov pred
rodič
commit
06dbdd234f

+ 235 - 276
AI/BattleAI/BattleAI.cpp

@@ -13,116 +13,23 @@ CBattleCallback * cbc;
 #define LOGL(text) print(text)
 #define LOGFL(text, formattingEl) print(boost::str(boost::format(text) % formattingEl))
 
-
-
-class StackWithBonuses : public IBonusBearer
+struct Priorities
 {
-public:
-	const CStack *stack;
-	mutable std::vector<Bonus> bonusesToAdd;
-
-	virtual const TBonusListPtr getAllBonuses(const CSelector &selector, const CSelector &limit, const CBonusSystemNode *root = NULL, const std::string &cachingStr = "") const OVERRIDE
-	{
-		TBonusListPtr ret = make_shared<BonusList>();
-		const TBonusListPtr originalList = stack->getAllBonuses(selector, limit, root, cachingStr);
-		boost::copy(*originalList, std::back_inserter(*ret));
-		BOOST_FOREACH(auto &bonus, bonusesToAdd)
-		{
-			if(selector(&bonus)  &&  (!limit || !limit(&bonus)))
-				ret->push_back(&bonus);
-		}
-
-		//TODO limiters?
-
-		return ret;
-	}
-};
+	double manaValue;
+	double generalResourceValueModifier;
+	std::vector<double> resourceTypeBaseValues;
+	std::function<double(const CStack *)> stackEvaluator;
 
-struct Skirmish
-{
-	const CStack *attacker, *defender;
-	int retaliationDamage, dealtDamage;
 
-	Skirmish(const CStack *Attacker, const CStack *Defender)
-		:attacker(Attacker), defender(Defender)
+	Priorities()
 	{
-		TDmgRange retal, dmg = cbc->battleEstimateDamage(attacker, defender, &retal);
-		dealtDamage = (dmg.first + dmg.second) / 2;
-		retaliationDamage = (retal.first + retal.second) / 2;
-
-		if(attacker->hasBonusOfType(Bonus::ADDITIONAL_ATTACK))
-			dealtDamage *= 2;
-		if(attacker->hasBonusOfType(Bonus::BLOCKS_RETALIATION) || defender->hasBonusOfType(Bonus::NO_RETALIATION))
-			retaliationDamage = 0;
-
-	}
-};
-
-CBattleAI::CBattleAI(void)
-	: side(-1), cb(NULL)
-{
-	print("created");
-}
-
-
-CBattleAI::~CBattleAI(void)
-{
-	print("destroyed");
-
-	if(cb)
-	{
-		//Restore previous state of CB - it may be shared with the main AI (like VCAI)
-		cb->waitTillRealize = wasWaitingForRealize;
-		cb->unlockGsWhenWaiting = wasUnlockingGs;
+		manaValue = 0.;
+		generalResourceValueModifier = 1.;
+		range::copy(VLC->objh->resVals, std::back_inserter(resourceTypeBaseValues));
+		stackEvaluator = [](const CStack*){ return 1.0; };
 	}
-}
-
-void CBattleAI::init( CBattleCallback * CB )
-{
-	print("init called, saving ptr to IBattleCallback");
-	cbc = cb = CB;
-	playerID = *CB->getPlayerID();; //TODO should be sth in callback
+} priorities;
 
-	wasWaitingForRealize = cb->waitTillRealize;
-	wasUnlockingGs = CB->unlockGsWhenWaiting;
-	CB->waitTillRealize = true;
-	CB->unlockGsWhenWaiting = false;
-}
-
-void CBattleAI::actionFinished(const BattleAction &action)
-{
-	print("actionFinished called");
-}
-
-void CBattleAI::actionStarted(const BattleAction &action)
-{
-	print("actionStarted called");
-}
-
-struct EnemyInfo
-{
-	const CStack * s;
-	int adi, adr;
-	std::vector<BattleHex> attackFrom; //for melee fight
-	EnemyInfo(const CStack * _s) : s(_s)
-	{}
-	void calcDmg(const CStack * ourStack)
-	{
-		TDmgRange retal, dmg = cbc->battleEstimateDamage(ourStack, s, &retal);
-		adi = (dmg.first + dmg.second) / 2;
-		adr = (retal.first + retal.second) / 2;
-	}
-
-	bool operator==(const EnemyInfo& ei) const
-	{
-		return s == ei.s;
-	}
-};
-
-bool isMoreProfitable(const EnemyInfo &ei1, const EnemyInfo& ei2)
-{
-	return (ei1.adi-ei1.adr) < (ei2.adi - ei2.adr);
-}
 
 int distToNearestNeighbour(BattleHex hex, const ReachabilityInfo::TDistances& dists, BattleHex *chosenHex = NULL)
 {
@@ -145,22 +52,6 @@ bool isCloser(const EnemyInfo & ei1, const EnemyInfo & ei2, const ReachabilityIn
 	return distToNearestNeighbour(ei1.s->position, dists) < distToNearestNeighbour(ei2.s->position, dists);
 }
 
-//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];
-}
-*/
-
 template <typename Container, typename Pred>
 auto sum(const Container & c, Pred p) -> decltype(p(*boost::begin(c)))
 {
@@ -173,177 +64,48 @@ auto sum(const Container & c, Pred p) -> decltype(p(*boost::begin(c)))
 	return ret;
 }
 
-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)
-		: endangered(Endangered)
-	{
-		sufferedDamage.fill(0);
 
-		BOOST_FOREACH(const CStack *enemy, cbc->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 = cbc->getReachability(enemy);
-			for(int i = 0; i < GameConstants::BFIELD_SIZE; i++)
-			{
-				if(enemyReachability.isReachable(i))
-				{
-					meleeAttackable[i] = true;
-					BOOST_FOREACH(auto n, BattleHex(i).neighbouringTiles())
-						meleeAttackable[n] = true;
-				}
-			}
 
-			//Gather possible assaults
-			for(int i = 0; i < GameConstants::BFIELD_SIZE; i++)
-			{
-				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));
-				}
-			}
-		}
-
-		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;
-			});
-		}
-	}
-};
-
-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 damageDiff() const
-	{
-		return damageDealt - damageReceived;
-	}
-
-	int attackValue() const
-	{
-		//TODO consider tactical advantage
-		return damageDiff();
-	}
-};
-
-template<typename Key, typename Val>
-const Val &getValOr(const std::map<Key, Val> &Map, const Key &key, const Val &defaultValue)
+CBattleAI::CBattleAI(void)
+	: side(-1), cb(NULL)
 {
-	auto i = Map.find(key);
-	if(i != Map.end())
-		return i->second;
-	else 
-		return defaultValue;
+	print("created");
 }
 
-struct HypotheticChangesToBattleState
-{
-	std::map<const CStack *, const IBonusBearer *> bonusesOfStacks;
-};
 
-struct PotentialTargets
+CBattleAI::~CBattleAI(void)
 {
-	std::vector<AttackPossibility> possibleAttacks;
-	std::vector<const CStack *> unreachableEnemies;
-
-	std::function<AttackPossibility(bool,BattleHex)>  GenerateAttackInfo; //args: shooting, destHex
-
-	PotentialTargets()
-	{}
+	print("destroyed");
 
-	PotentialTargets(const CStack *attacker, const HypotheticChangesToBattleState &state = HypotheticChangesToBattleState())
+	if(cb)
 	{
-		auto dists = cbc->battleGetDistances(attacker);
-		std::vector<BattleHex> avHexes = cbc->battleGetAvailableHexes(attacker, false);
-
-		BOOST_FOREACH(const CStack *enemy, cbc->battleGetStacks())
-		{
-			//Consider only stacks of different owner
-			if(enemy->attackerOwned == attacker->attackerOwned)
-				continue;
-			
-			GenerateAttackInfo = [&](bool shooting, BattleHex hex) -> AttackPossibility
-			{
-				auto bai = BattleAttackInfo(attacker, enemy, shooting);
-				bai.attackerBonuses = getValOr(state.bonusesOfStacks, bai.attacker, static_cast<const IBonusBearer *>(bai.attacker));
-				bai.defenderBonuses = getValOr(state.bonusesOfStacks, bai.defender, static_cast<const IBonusBearer *>(bai.defender));
-
-				AttackPossibility ap = {enemy, hex, bai, 0, 0};
-				if(hex.isValid())
-				{
-					assert(dists[hex] <= attacker->Speed());
-					ap.attack.chargedFields = dists[hex];
-				}
-
-				std::pair<ui32, ui32> retaliation;
-				auto attackDmg = cbc->battleEstimateDamage(ap.attack, &retaliation);
-				ap.damageDealt = (attackDmg.first + attackDmg.second) / 2;
-				ap.damageReceived = (retaliation.first + retaliation.second) / 2;
-				//TODO other damage related to attack (eg. fire shield and other abilities)
-				//TODO limit max damage by total stacks health (dealing 100000 dmg to single Pikineer is not that effective)
-
-				return ap;
-			};
-
-			if(cbc->battleCanShoot(attacker, enemy->position))
-			{
-				possibleAttacks.push_back(GenerateAttackInfo(true, BattleHex::INVALID));
-			}
-			else
-			{
-				BOOST_FOREACH(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);
-			}
-		}
+		//Restore previous state of CB - it may be shared with the main AI (like VCAI)
+		cb->waitTillRealize = wasWaitingForRealize;
+		cb->unlockGsWhenWaiting = wasUnlockingGs;
 	}
+}
 
-	AttackPossibility 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.damageDiff(); } );
-	}
+void CBattleAI::init( CBattleCallback * CB )
+{
+	print("init called, saving ptr to IBattleCallback");
+	cbc = cb = CB;
+	playerID = *CB->getPlayerID();; //TODO should be sth in callback
 
-	int bestActionValue() const
-	{
-		if(possibleAttacks.empty())
-			return 0;
+	wasWaitingForRealize = cb->waitTillRealize;
+	wasUnlockingGs = CB->unlockGsWhenWaiting;
+	CB->waitTillRealize = true;
+	CB->unlockGsWhenWaiting = false;
+}
 
-		return bestAction().damageDiff();
-	}
-};
+static bool thereRemainsEnemy()
+{
+	return cbc->battleGetStacks(CBattleInfoEssentials::ONLY_ENEMY).size();
+}
 
 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)
 	try
 	{
@@ -354,6 +116,9 @@ BattleAction CBattleAI::activeStack( const CStack * stack )
 		if(cb->battleCanCastSpell())
 			attemptCastingSpell();
 
+		if(!thereRemainsEnemy())
+			return BattleAction();
+
 		if(auto action = considerFleeingOrSurrendering())
 			return *action;
 
@@ -364,6 +129,8 @@ BattleAction CBattleAI::activeStack( const CStack * stack )
 		}
 
 		PotentialTargets targets(stack);
+
+
 		if(targets.possibleAttacks.size())
 		{
 			auto hlp = targets.bestAction();
@@ -392,12 +159,22 @@ BattleAction CBattleAI::activeStack( const CStack * stack )
 	}
 	catch(std::exception &e)
 	{
-        logAi->errorStream() << "Exception occurred in " << __FUNCTION__ << " " << e.what();
+		logAi->errorStream() << "Exception occurred in " << __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");
@@ -591,6 +368,8 @@ struct CurrentOffensivePotential
 		return ourPotential - enemyPotential;
 	}
 };
+
+
 // 
 // //set has its own order, so remove_if won't work. TODO - reuse for map
 // template<typename Elem, typename Predicate>
@@ -764,3 +543,183 @@ boost::optional<BattleAction> CBattleAI::considerFleeingOrSurrendering()
 	}
 	return boost::none;
 }
+
+ThreatMap::ThreatMap(const CStack *Endangered) : endangered(Endangered)
+{
+	sufferedDamage.fill(0);
+
+	BOOST_FOREACH(const CStack *enemy, cbc->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 = cbc->getReachability(enemy);
+		for(int i = 0; i < GameConstants::BFIELD_SIZE; i++)
+		{
+			if(enemyReachability.isReachable(i))
+			{
+				meleeAttackable[i] = true;
+				BOOST_FOREACH(auto n, BattleHex(i).neighbouringTiles())
+					meleeAttackable[n] = true;
+			}
+		}
+
+		//Gather possible assaults
+		for(int i = 0; i < GameConstants::BFIELD_SIZE; i++)
+		{
+			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));
+			}
+		}
+	}
+
+	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;
+		});
+	}
+}
+
+const TBonusListPtr StackWithBonuses::getAllBonuses(const CSelector &selector, const CSelector &limit, const CBonusSystemNode *root /*= NULL*/, const std::string &cachingStr /*= ""*/) const 
+{
+	TBonusListPtr ret = make_shared<BonusList>();
+	const TBonusListPtr originalList = stack->getAllBonuses(selector, limit, root, cachingStr);
+	boost::copy(*originalList, std::back_inserter(*ret));
+	BOOST_FOREACH(auto &bonus, bonusesToAdd)
+	{
+		if(selector(&bonus)  &&  (!limit || !limit(&bonus)))
+			ret->push_back(&bonus);
+	}
+
+	//TODO limiters?
+
+	return ret;
+}
+
+int AttackPossibility::damageDiff() const
+{
+	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->counterAttacks);
+	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) || 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 = cbc->battleEstimateDamage(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);
+
+	BOOST_FOREACH(const CStack *enemy, cbc->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(cbc->battleCanShoot(attacker, enemy->position))
+		{
+			possibleAttacks.push_back(GenerateAttackInfo(true, BattleHex::INVALID));
+		}
+		else
+		{
+			BOOST_FOREACH(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);
+		}
+	}
+}
+
+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(ourStack, s, &retal);
+	adi = (dmg.first + dmg.second) / 2;
+	adr = (retal.first + retal.second) / 2;
+}

+ 102 - 0
AI/BattleAI/BattleAI.h

@@ -1,9 +1,111 @@
 #pragma once
 
 #include "../../lib/BattleHex.h"
+#include "../../lib/HeroBonus.h"
+#include "../../lib/CBattleCallback.h"
 
 class CSpell;
 
+
+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 = NULL, const std::string &cachingStr = "") const OVERRIDE;
+};
+
+struct EnemyInfo
+{
+	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;
+	}
+};
+
+
+//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 damageDiff() const;
+	int attackValue() const;
+
+	static AttackPossibility evaluate(const BattleAttackInfo &AttackInfo, const HypotheticChangesToBattleState &state, BattleHex hex);
+};
+
+template<typename Key, typename Val, typename Val2>
+const Val &getValOr(const std::map<Key, Val> &Map, const Key &key, const Val2 &defaultValue)
+{
+	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;
+};
+
 class CBattleAI : public CBattleGameInterface
 {
 	int side;

+ 13 - 6
client/CMT.cpp

@@ -78,7 +78,8 @@ std::queue<SDL_Event> events;
 boost::mutex eventsM;
 
 bool gNoGUI = false;
-static bool gOnlyAI = false;
+static po::variables_map vm;
+
 //static bool setResolution = false; //set by event handling thread after resolution is adjusted
 
 static bool ermInteractiveMode = false; //structurize when time is right
@@ -226,12 +227,12 @@ int main(int argc, char** argv)
 		("start", po::value<std::string>(), "starts game from saved StartInfo file")
 		("onlyAI", "runs without human player, all players will be default AI")
 		("noGUI", "runs without GUI, implies --onlyAI")
+		("ai", po::value<std::vector<std::string>>(), "AI to be used for the player, can be specified several times for the consecutive players")
 		("oneGoodAI", "puts one default AI and the rest will be EmptyAI")
 		("autoSkip", "automatically skip turns in GUI")
 		("disable-video", "disable video player")
 		("nointro,i", "skips intro movies");
 
-	po::variables_map vm;
 	if(argc > 1)
 	{
 		try
@@ -258,7 +259,7 @@ int main(int argc, char** argv)
 	if(vm.count("noGUI"))
 	{
 		gNoGUI = true;
-		vm["onlyAI"];
+		vm.insert(std::pair<std::string, po::variable_value>("onlyAI", po::variable_value()));
 	}
 
 	//Set environment vars to make window centered. Sometimes work, sometimes not. :/
@@ -375,7 +376,6 @@ int main(int argc, char** argv)
 
 	if(!vm.count("battle"))
 	{
-		gOnlyAI = vm.count("onlyAI");
 		Settings session = settings.write["session"];
 		session["autoSkip"].Bool()  = vm.count("autoSkip");
 		session["oneGoodAI"].Bool() = vm.count("oneGoodAI");
@@ -566,7 +566,7 @@ void processCommand(const std::string &message)
 	}
 	else if(cn == "onlyai")
 	{
-		gOnlyAI = true;
+		vm.insert(std::pair<std::string, po::variable_value>("onlyAI", po::variable_value()));
 	}
 	else if (cn == "ai")
 	{
@@ -917,11 +917,18 @@ static void listenForEvents()
 
 void startGame(StartInfo * options, CConnection *serv/* = NULL*/)
 {
-	if(gOnlyAI)
+	if(vm.count("onlyAI"))
 	{
+		auto ais = vm["ai"].as<std::vector<std::string>>();
+
+		int i = 0;
+
+
 		for(auto it = options->playerInfos.begin(); it != options->playerInfos.end(); ++it)
 		{
 			it->second.playerID = PlayerSettings::PLAYER_AI;
+			if(i < ais.size())
+				it->second.name = ais[i++];
 		}
 	}
 

+ 13 - 5
client/Client.cpp

@@ -410,10 +410,18 @@ void CClient::newGame( CConnection *con, StartInfo *si )
 		{
 			auto cbc = make_shared<CBattleCallback>(gs, color, this);
 			battleCallbacks[color] = cbc;
-			if(color == PlayerColor(0))
-				battleints[color] = CDynLibHandler::getNewBattleAI(settings["server"]["neutralAI"].String());
-			else
-				battleints[color] = CDynLibHandler::getNewBattleAI("StupidAI");
+
+			std::string AItoGive = it->second.name;
+			if(AItoGive.empty())
+			{
+				if(color == PlayerColor(0))
+					battleints[color] = CDynLibHandler::getNewBattleAI(settings["server"]["neutralAI"].String());
+				else
+					battleints[color] = CDynLibHandler::getNewBattleAI("StupidAI");
+			}
+
+
+			battleints[color] = CDynLibHandler::getNewBattleAI(AItoGive);
 			battleints[color]->init(cbc.get());
 		}
 	}
@@ -802,7 +810,7 @@ void CServerHandler::callServer()
 {
 	setThreadName("CServerHandler::callServer");
 	std::string logName = VCMIDirs::get().localPath() + "/server_log.txt";
-	std::string comm = VCMIDirs::get().serverPath() + " " + port + " > " + logName;
+	std::string comm = VCMIDirs::get().serverPath() + " --port=" + port + " > " + logName;
 	int result = std::system(comm.c_str());
 	if (result == 0)
         logNetwork->infoStream() << "Server closed correctly";

+ 38 - 16
lib/BattleState.cpp

@@ -1129,33 +1129,55 @@ std::string CStack::nodeName() const
 	return oss.str();
 }
 
-void CStack::prepareAttacked(BattleStackAttacked &bsa) const
+std::pair<int,int> CStack::countKilledByAttack(int damageReceived) const
 {
-	bsa.killedAmount = bsa.damageAmount / MaxHealth();
-	unsigned damageFirst = bsa.damageAmount % MaxHealth();
+	int killedCount = 0;
+	int newRemainingHP = 0;
 
-	if (bsa.damageAmount && vstd::contains(state, EBattleStackState::CLONED)) // block ability should not kill clone (0 damage)
-	{
-		bsa.killedAmount = count;
-		bsa.flags |= BattleStackAttacked::CLONE_KILLED;
-		return; // no rebirth I believe
-	}
+	killedCount = damageReceived / MaxHealth();
+	unsigned damageFirst = damageReceived % MaxHealth();
 
-	if( firstHPleft <= damageFirst )
+	if (damageReceived && vstd::contains(state, EBattleStackState::CLONED)) // block ability should not kill clone (0 damage)
 	{
-		bsa.killedAmount++;
-		bsa.newHP = firstHPleft + MaxHealth() - damageFirst;
+		killedCount = count;
 	}
 	else
 	{
-		bsa.newHP = firstHPleft - damageFirst;
+		if( firstHPleft <= damageFirst )
+		{
+			killedCount++;
+			newRemainingHP = firstHPleft + MaxHealth() - damageFirst;
+		}
+		else
+		{
+			newRemainingHP = firstHPleft - damageFirst;
+		}
 	}
 
-	if(count <= bsa.killedAmount) //stack killed
+	return std::make_pair(killedCount, newRemainingHP);
+}
+
+void CStack::prepareAttacked(BattleStackAttacked &bsa, boost::optional<int> customCount /*= boost::none*/) const
+{
+	auto afterAttack = countKilledByAttack(bsa.damageAmount);
+
+	bsa.killedAmount = afterAttack.first;
+	bsa.newHP = afterAttack.second;
+
+
+	if (bsa.damageAmount && vstd::contains(state, EBattleStackState::CLONED)) // block ability should not kill clone (0 damage)
+	{
+		bsa.flags |= BattleStackAttacked::CLONE_KILLED;
+		return; // no rebirth I believe
+	}
+
+	const int countToUse = customCount ? *customCount : count;
+
+	if(countToUse <= bsa.killedAmount) //stack killed
 	{
 		bsa.newAmount = 0;
 		bsa.flags |= BattleStackAttacked::KILLED;
-		bsa.killedAmount = count; //we cannot kill more creatures than we have
+		bsa.killedAmount = countToUse; //we cannot kill more creatures than we have
 
 		int resurrectFactor = valOfBonuses(Bonus::REBIRTH);
 		if (resurrectFactor > 0 && casts) //there must be casts left
@@ -1177,7 +1199,7 @@ void CStack::prepareAttacked(BattleStackAttacked &bsa) const
 	}
 	else
 	{
-		bsa.newAmount = count - bsa.killedAmount;
+		bsa.newAmount = countToUse - bsa.killedAmount;
 	}
 }
 

+ 2 - 1
lib/BattleState.h

@@ -193,7 +193,8 @@ public:
 	bool coversPos(BattleHex position) const; //checks also if unit is double-wide
 	std::vector<BattleHex> getSurroundingHexes(BattleHex attackerPos = BattleHex::INVALID) const; // get six or 8 surrounding hexes depending on creature size
 
-	void prepareAttacked(BattleStackAttacked &bsa) const; //requires bsa.damageAmout filled
+	std::pair<int,int> countKilledByAttack(int damageReceived) const; //returns pair<killed count, new left HP>
+	void prepareAttacked(BattleStackAttacked &bsa, boost::optional<int> customCount = boost::none) const; //requires bsa.damageAmout filled
 
 	template <typename Handler> void serialize(Handler &h, const int version)
 	{

+ 4 - 2
lib/CBattleCallback.cpp

@@ -1017,7 +1017,7 @@ std::pair<ui32, ui32> CBattleInfoCallback::battleEstimateDamage(const BattleAtta
 			{
 				BattleStackAttacked bsa;
 				bsa.damageAmount = ret.*pairElems[i];
-				bai.defender->prepareAttacked(bsa);
+				bai.defender->prepareAttacked(bsa, bai.defenderCount);
 
 				auto retaliationAttack = bai.reverse();
 				retaliationAttack.attackerCount = bsa.newAmount;
@@ -2329,6 +2329,8 @@ BattleAttackInfo::BattleAttackInfo(const CStack *Attacker, const CStack *Defende
 	defenderPosition = Defender->position;
 
 	attackerCount = Attacker->count;
+	defenderCount = Defender->count;
+
 	shooting = Shooting;
 	chargedFields = 0;
 
@@ -2343,8 +2345,8 @@ BattleAttackInfo BattleAttackInfo::reverse() const
 	std::swap(ret.attacker, ret.defender);
 	std::swap(ret.attackerBonuses, ret.defenderBonuses);
 	std::swap(ret.attackerPosition, ret.defenderPosition);
+	std::swap(ret.attackerCount, ret.defenderCount);
 
-	ret.attackerCount = ret.attacker->count;
 	ret.shooting = false;
 	ret.chargedFields = 0;
 	ret.luckyHit = ret.ballistaDoubleDamage = ret.deathBlow = false;

+ 1 - 1
lib/CBattleCallback.h

@@ -198,7 +198,7 @@ struct DLL_LINKAGE BattleAttackInfo
 	const CStack *attacker, *defender;
 	BattleHex attackerPosition, defenderPosition;
 
-	int attackerCount;
+	int attackerCount, defenderCount;
 	bool shooting;
 	int chargedFields;
 

+ 4 - 0
lib/StartInfo.h

@@ -93,6 +93,10 @@ struct StartInfo
         logGlobal->errorStream() << "Cannot find info about player " << no <<". Throwing...";
 		throw std::runtime_error("Cannot find info about player");
 	}
+	const PlayerSettings & getIthPlayersSettings(PlayerColor no) const
+	{
+		return const_cast<StartInfo&>(*this).getIthPlayersSettings(no);
+	}
 
 	PlayerSettings *getPlayersSettings(const ui8 nameID)
 	{

+ 53 - 16
server/CGameHandler.cpp

@@ -24,6 +24,7 @@
 #include "../lib/VCMIDirs.h"
 #include "../client/CSoundBase.h"
 #include "CGameHandler.h"
+#include "CVCMIServer.h"
 #include "../lib/CCreatureSet.h"
 #include "../lib/CThreadHelper.h"
 #include "../lib/GameConstants.h"
@@ -455,6 +456,15 @@ void CGameHandler::endBattle(int3 tile, const CGHeroInstance *hero1, const CGHer
 
 
 	CasualtiesAfterBattle cab1(bEndArmy1, gs->curB), cab2(bEndArmy2, gs->curB); //calculate casualties before deleting battle
+
+	if(finishingBattle->duel)
+	{
+		duelFinished();
+		sendAndApply(battleResult.data); //after this point casualties objects are destroyed
+		return;
+	}
+
+
 	ChangeSpells cs; //for Eagle Eye
 
 	if(finishingBattle->winnerHero)
@@ -595,15 +605,6 @@ void CGameHandler::endBattle(int3 tile, const CGHeroInstance *hero1, const CGHer
 		sendAndApply(&iw);
 		sendAndApply(&cs);
 	}
-
-	if(finishingBattle->duel)
-	{
-		BattleResultsApplied resultsApplied;
-		resultsApplied.player1 = finishingBattle->victor;
-		resultsApplied.player2 = finishingBattle->loser;
-		sendAndApply(&resultsApplied);
-		return;
-	}
 	
 	cab1.takeFromArmy(this); cab2.takeFromArmy(this); //take casualties after battle is deleted
 
@@ -665,13 +666,6 @@ void CGameHandler::battleAfterLevelUp( const BattleResult &result )
 
 	setBattle(nullptr);
 
-	if(finishingBattle->duel)
-	{
-		CSaveFile resultFile("result.vdrst");
-		resultFile << *battleResult.data;
-		return;
-	}
-
 	if(visitObjectAfterVictory && result.winner==0)
 	{
 		logGlobal->traceStream() << "post-victory visit";
@@ -6187,6 +6181,49 @@ bool CGameHandler::isVisitCoveredByAnotherQuery(const CGObjectInstance *obj, con
 	return true;
 }
 
+void CGameHandler::duelFinished()
+{
+	auto si = getStartInfo();
+	auto getName = [&](int i){ return si->getIthPlayersSettings(gs->curB->sides[i]).name; };
+
+	int casualtiesPoints = 0;
+	logGlobal->debugStream() << boost::format("Winner side %d\nWinner casualties:") 
+		% (int)battleResult.data->winner;
+
+	for(auto i = battleResult.data->casualties[battleResult.data->winner].begin(); i != battleResult.data->casualties[battleResult.data->winner].end(); i++)
+	{
+		const CCreature *c = VLC->creh->creatures[i->first];
+		logGlobal->debugStream() << boost::format("\t* %d of %s") % i->second % c->namePl;
+		casualtiesPoints += c->AIValue * i->second;
+	}
+	logGlobal->debugStream() << boost::format("Total casualties points: %d") % casualtiesPoints;
+
+
+	time_t timeNow;
+	time(&timeNow);
+
+	std::ofstream out(cmdLineOptions["resultsFile"].as<std::string>(), std::ios::app);
+	if(out)
+	{
+		out << boost::format("%s\t%s\t%s\t%d\t%d\t%d\t%s\n") % si->mapname % getName(0) % getName(1)
+			% battleResult.data->winner % battleResult.data->result % casualtiesPoints 
+			% asctime(localtime(&timeNow));
+	}
+	else
+	{
+		logGlobal->errorStream() << "Cannot open to write " << cmdLineOptions["resultsFile"].as<std::string>();
+	}
+
+	CSaveFile resultFile("result.vdrst");
+	resultFile << *battleResult.data;
+
+	BattleResultsApplied resultsApplied;
+	resultsApplied.player1 = finishingBattle->victor;
+	resultsApplied.player2 = finishingBattle->loser;
+	sendAndApply(&resultsApplied);
+	return;
+}
+
 CasualtiesAfterBattle::CasualtiesAfterBattle(const CArmedInstance *army, BattleInfo *bat)
 {
 	heroWithDeadCommander = ObjectInstanceID();

+ 1 - 0
server/CGameHandler.h

@@ -121,6 +121,7 @@ public:
 	void checkForBattleEnd();
 	void setupBattle(int3 tile, const CArmedInstance *armies[2], const CGHeroInstance *heroes[2], bool creatureBank, const CGTownInstance *town);
 	void setBattleResult(BattleResult::EResult resultType, int victoriusSide);
+	void duelFinished();
 
 	CGameHandler(void);
 	~CGameHandler(void);

+ 33 - 7
server/CVCMIServer.cpp

@@ -43,6 +43,8 @@ namespace intpr = boost::interprocess;
 bool end2 = false;
 int port = 3030;
 
+boost::program_options::variables_map cmdLineOptions;
+
 /*
  * CVCMIServer.cpp, part of VCMI engine
  *
@@ -508,21 +510,45 @@ void CVCMIServer::loadGame()
 	gh.run(true);
 }
 
+static void handleCommandOptions(int argc, char *argv[])
+{
+	namespace po = boost::program_options;
+	po::options_description opts("Allowed options");
+	opts.add_options()
+		("help,h", "display help and exit")
+		("version,v", "display version information and exit")
+		("port", po::value<int>()->default_value(3030), "port at which server will listen to connections from client")
+		("resultsFile", po::value<std::string>()->default_value("./results.txt"), "file to which the battle result will be appended. Used only in the DUEL mode.");
+
+	if(argc > 1)
+	{
+		try
+		{
+			po::store(po::parse_command_line(argc, argv, opts), cmdLineOptions);
+		}
+		catch(std::exception &e) 
+		{
+			std::cerr << "Failure during parsing command-line options:\n" << e.what() << std::endl;
+		}
+	}
+
+	po::notify(cmdLineOptions);
+}
+
 int main(int argc, char** argv)
 {
 	console = new CConsoleHandler;
 	CBasicLogConfigurator logConfig(VCMIDirs::get().localPath() + "/VCMI_Server_log.txt", console);
-    logConfig.configureDefault();
-	if(argc > 1)
-	{
-		port = boost::lexical_cast<int>(argv[1]);
-    }
+	logConfig.configureDefault();
 
 	preinitDLL(console);
     settings.init();
-    logConfig.configure();
+	logConfig.configure();
+
+	handleCommandOptions(argc, argv);
+	port = cmdLineOptions["port"].as<int>();
+	logNetwork->infoStream() << "Port " << port << " will be used.";
 
-    logNetwork->infoStream() << "Port " << port << " will be used.";
 	loadDLLClasses();
 	srand ( (ui32)time(NULL) );
 	try

+ 2 - 0
server/CVCMIServer.h

@@ -96,3 +96,5 @@ public:
 	void sendPack(CConnection * pc, const CPackForSelectionScreen & pack);
 	void startListeningThread(CConnection * pc);
 };
+
+extern boost::program_options::variables_map cmdLineOptions;