reworks battleAI preliminary spell target choice - review fix

This commit is contained in:
Opuszek
2026-03-14 20:33:36 +01:00
parent 2db8cba51e
commit f18e7a67e1
5 changed files with 360 additions and 360 deletions
+2 -2
View File
@@ -160,7 +160,7 @@ std::optional<PossibleSpellcast> BattleEvaluator::findBestCreatureSpell(const CS
continue;
spells::BattleCast temp(cb->getBattle(battleID).get(), stack, spells::Mode::CREATURE_ACTIVE, spell);
for(auto & target : SpellTargetEvaluator::getViableTargets(spell->battleMechanics(&temp).get()))
for(const auto & target : SpellTargetEvaluator::getViableTargets(spell->battleMechanics(&temp).get()))
{
PossibleSpellcast ps;
ps.dest = target;
@@ -539,7 +539,7 @@ bool BattleEvaluator::attemptCastingSpell(const CStack * activeStack)
{
spells::BattleCast temp(cb->getBattle(battleID).get(), hero, spells::Mode::HERO, spell);
for(auto & target : SpellTargetEvaluator::getViableTargets(spell->battleMechanics(&temp).get()))
for(const auto & target : SpellTargetEvaluator::getViableTargets(spell->battleMechanics(&temp).get()))
{
PossibleSpellcast ps;
ps.dest = target;
+189 -184
View File
@@ -8,234 +8,239 @@
*
*/
#include "StdInc.h"
#include "../../lib/CStack.h"
#include "../../lib/battle/CBattleInfoCallback.h"
#include "../../lib/spells/Problem.h"
#include "../../lib/CRandomGenerator.h"
#include "SpellTargetsEvaluator.h"
#include <vcmi/spells/Spell.h>
#include "../../lib/spells/Problem.h"
#include "../../lib/battle/CBattleInfoCallback.h"
#include "../../lib/CStack.h"
using namespace spells;
std::vector<Target> SpellTargetEvaluator::getViableTargets(Mechanics * spellMechanics)
std::vector<Target> SpellTargetEvaluator::getViableTargets(const Mechanics * spellMechanics)
{
std::vector<Target> result;
if (!spellMechanics)
{
logGlobal->error("SpellTargetEvaluator received empty spellMechanics. Should not occur!");
return result;
}
std::vector<AimType> targetTypes = spellMechanics->getTargetTypes();
if (targetTypes.size() != 1) //multi-destination spells like teleport are not implemented - they would be too resource-intensive
return result;
std::vector<Target> result;
std::vector<AimType> targetTypes = spellMechanics->getTargetTypes();
if(targetTypes.size() != 1) //TODO: support for multi-destination spells
return result;
auto targetType = targetTypes.front();
auto targetType = targetTypes.front();
switch(targetType)
{
case AimType::CREATURE:
return allTargetableCreatures(spellMechanics);
case AimType::LOCATION:
{
if (spellMechanics->isNeutralSpell())
return defaultLocationSpellHeuristics(spellMechanics); // theoretically anything can be a useful destination, so we balance performance and validity
else
return theBestLocationCasts(spellMechanics);
}
case AimType::NO_TARGET:
return std::vector<Target>(1); //default-constructed target means cast without destination
default:
return result;
}
switch(targetType)
{
case AimType::CREATURE://TODO: support for multi-destination spells
return allTargetableCreatures(spellMechanics);
case AimType::LOCATION:
{
if(spellMechanics->isNeutralSpell())
return defaultLocationSpellHeuristics(
spellMechanics
); // theoretically anything can be a useful destination, so we balance performance and validity
else
return theBestLocationCasts(spellMechanics);
}
case AimType::NO_TARGET:
return std::vector<Target>(1); //default-constructed target means cast without destination
default:
return result;
}
}
std::vector<Target> SpellTargetEvaluator::defaultLocationSpellHeuristics(spells::Mechanics * spellMechanics)
std::vector<Target> SpellTargetEvaluator::defaultLocationSpellHeuristics(const spells::Mechanics * spellMechanics)
{
std::vector<Target> result = allTargetableCreatures(spellMechanics);
auto units = spellMechanics->battle()->battleGetAllUnits(false);
for(const auto * unit : units) //insert a random surrounding hex
{
auto surroundingHexes = unit->getSurroundingHexes();
if (!surroundingHexes.empty())
{
auto randomSurroundingHex = surroundingHexes.at(rand() % surroundingHexes.size()); // don't think this method bias matter with such small numbers
addIfCanBeCast(spellMechanics, randomSurroundingHex, result);
}
}
return result;
std::vector<Target> result = allTargetableCreatures(spellMechanics);
auto units = spellMechanics->battle()->battleGetAllUnits(false);
for(const auto * unit : units) //insert a random surrounding hex
{
auto surroundingHexes = unit->getSurroundingHexes();
if(!surroundingHexes.empty())
{
auto randomSurroundingHex = *RandomGeneratorUtil::nextItem(surroundingHexes, CRandomGenerator::getDefault()); // don't think this method bias matter with such small numbers
addIfCanBeCast(spellMechanics, randomSurroundingHex, result);
}
}
return result;
}
std::vector<Target> SpellTargetEvaluator::allTargetableCreatures(spells::Mechanics * spellMechanics) {
std::vector<Target> result;
auto units = spellMechanics->battle()->battleGetAllUnits(false);
for(const auto * unit : units)
addIfCanBeCast(spellMechanics, unit->getPosition(), result);
return result;
}
std::vector<Target> SpellTargetEvaluator::theBestLocationCasts(spells::Mechanics * spellMechanics)
std::vector<Target> SpellTargetEvaluator::allTargetableCreatures(const spells::Mechanics * spellMechanics)
{
std::vector<Target> result;
std::map<BattleHex,std::set<const CStack *>> allCasts;
std::map<BattleHex,std::set<const CStack *>> bestCasts;
for(int i = 0; i < GameConstants::BFIELD_SIZE; i++)
{
BattleHex dest(i);
if(canBeCastAt(spellMechanics, dest))
{
Target target;
target.emplace_back(dest);
auto temp = spellMechanics->getAffectedStacks(target);
std::set<const CStack *> affectedStacks(temp.begin(), temp.end());
allCasts[dest] = affectedStacks;
}
}
for (auto const& cast : allCasts)
{
std::set<BattleHex> worseCasts;
if(isCastHarmful(spellMechanics, cast.second))
continue;
bool isBestCast = true;
for(auto const& bestCast : bestCasts)
{
Compare compare = compareAffectedStacks(spellMechanics, cast.second, bestCast.second);
if (compare == Compare::WORSE || compare == Compare::EQUAL)
{
isBestCast = false;
break;
}
if (compare == Compare::BETTER)
{
worseCasts.insert(bestCast.first);
}
}
if (isBestCast)
{
bestCasts.insert(cast);
for (BattleHex worseCast : worseCasts)
bestCasts.erase(worseCast);
}
}
for (auto const& cast : bestCasts)
{
Destination des(cast.first);
result.push_back({des});
}
return result;
std::vector<Target> result;
auto units = spellMechanics->battle()->battleGetAllUnits(false);
for(const auto * unit : units)
addIfCanBeCast(spellMechanics, unit->getPosition(), result);
return result;
}
bool SpellTargetEvaluator::isCastHarmful(spells::Mechanics * spellMechanics, std::set<const CStack *> affectedStacks)
std::vector<Target> SpellTargetEvaluator::theBestLocationCasts(const spells::Mechanics * spellMechanics)
{
std::vector<Target> result;
std::map<BattleHex, std::set<const CStack *>> allCasts;
std::map<BattleHex, std::set<const CStack *>> bestCasts;
for(int i = 0; i < GameConstants::BFIELD_SIZE; i++)
{
BattleHex dest(i);
if(canBeCastAt(spellMechanics, dest))
{
Target target;
target.emplace_back(dest);
auto temp = spellMechanics->getAffectedStacks(target);
std::set<const CStack *> affectedStacks(temp.begin(), temp.end());
allCasts[dest] = affectedStacks;
}
}
for(const auto & cast : allCasts)
{
std::set<BattleHex> worseCasts;
if(isCastHarmful(spellMechanics, cast.second))
continue;
bool isBestCast = true;
for(const auto & bestCast : bestCasts)
{
Compare compare = compareAffectedStacks(spellMechanics, cast.second, bestCast.second);
if(compare == Compare::WORSE || compare == Compare::EQUAL)
{
isBestCast = false;
break;
}
if(compare == Compare::BETTER)
{
worseCasts.insert(bestCast.first);
}
}
if(isBestCast)
{
bestCasts.insert(cast);
for(BattleHex worseCast : worseCasts)
bestCasts.erase(worseCast);
}
}
for(const auto & cast : bestCasts)
{
Destination des(cast.first);
result.push_back({des});
}
return result;
}
bool SpellTargetEvaluator::isCastHarmful(const spells::Mechanics * spellMechanics, const std::set<const CStack *> & affectedStacks)
{
bool isAffectedAlly = false;
bool isAffectedEnemy = false;
bool isAffectedAlly = false;
bool isAffectedEnemy = false;
for (const CStack * affectedUnit : affectedStacks)
{
if(affectedUnit->unitSide() == spellMechanics->casterSide)
isAffectedAlly = true;
else
isAffectedEnemy = true;
}
for(const CStack * affectedUnit : affectedStacks)
{
if(affectedUnit->unitSide() == spellMechanics->casterSide)
isAffectedAlly = true;
else
isAffectedEnemy = true;
}
return (spellMechanics->isPositiveSpell() && !isAffectedAlly) || (spellMechanics->isNegativeSpell() && !isAffectedEnemy);
return (spellMechanics->isPositiveSpell() && !isAffectedAlly) || (spellMechanics->isNegativeSpell() && !isAffectedEnemy);
}
SpellTargetEvaluator::Compare SpellTargetEvaluator::compareAffectedStacks(spells::Mechanics * spellMechanics, std::set<const CStack *> newCast, std::set<const CStack *> oldCast)
SpellTargetEvaluator::Compare SpellTargetEvaluator::compareAffectedStacks(
const spells::Mechanics * spellMechanics, const std::set<const CStack *> & newCast, const std::set<const CStack *> & oldCast)
{
if (newCast.size() == oldCast.size())
return newCast == oldCast ? Compare::EQUAL : Compare::DIFFERENT;
if(newCast.size() == oldCast.size())
return newCast == oldCast ? Compare::EQUAL : Compare::DIFFERENT;
auto getAlliedUnits = [&spellMechanics] (std::set<const CStack *> allUnits) -> std::set<const CStack *> {
std::set<const CStack *> alliedUnits;
for (auto stack : allUnits) {
if (stack->unitSide() == spellMechanics->casterSide)
alliedUnits.insert(stack);
}
return alliedUnits;
};
auto getAlliedUnits = [&spellMechanics](const std::set<const CStack *> & allUnits) -> std::set<const CStack *>
{
std::set<const CStack *> alliedUnits;
for(auto stack : allUnits)
{
if(stack->unitSide() == spellMechanics->casterSide)
alliedUnits.insert(stack);
}
return alliedUnits;
};
auto getEnemyUnits = [&spellMechanics] (std::set<const CStack *> allUnits) -> std::set<const CStack *> {
std::set<const CStack *> enemyUnits;
for (auto stack : allUnits) {
if (stack->unitSide() != spellMechanics->casterSide)
enemyUnits.insert(stack);
}
return enemyUnits;
};
auto getEnemyUnits = [&spellMechanics](const std::set<const CStack *> & allUnits) -> std::set<const CStack *>
{
std::set<const CStack *> enemyUnits;
for(auto stack : allUnits)
{
if(stack->unitSide() != spellMechanics->casterSide)
enemyUnits.insert(stack);
}
return enemyUnits;
};
Compare alliedSubsetComparison = compareAffectedStacksSubset(spellMechanics, getAlliedUnits(newCast), getAlliedUnits(oldCast));
Compare enemySubsetComparison = compareAffectedStacksSubset(spellMechanics, getEnemyUnits(newCast), getEnemyUnits(oldCast));
Compare alliedSubsetComparison = compareAffectedStacksSubset(spellMechanics, getAlliedUnits(newCast), getAlliedUnits(oldCast));
Compare enemySubsetComparison = compareAffectedStacksSubset(spellMechanics, getEnemyUnits(newCast), getEnemyUnits(oldCast));
if(spellMechanics->isPositiveSpell())
enemySubsetComparison = reverse(enemySubsetComparison);
else if(spellMechanics->isNegativeSpell())
alliedSubsetComparison = reverse(alliedSubsetComparison);
if (spellMechanics->isPositiveSpell())
enemySubsetComparison = reverse(enemySubsetComparison);
else if (spellMechanics->isNegativeSpell())
alliedSubsetComparison = reverse(alliedSubsetComparison);
std::set<Compare> comparisonResults = {alliedSubsetComparison, enemySubsetComparison};
std::set<std::set<Compare>> possibleBetterResults = {
{Compare::BETTER, Compare::BETTER},
{Compare::BETTER, Compare::EQUAL }
};
std::set<std::set<Compare>> possibleWorstResults = {
{Compare::WORSE, Compare::WORSE},
{Compare::WORSE, Compare::EQUAL}
};
std::set<Compare>comparisonResults = {alliedSubsetComparison, enemySubsetComparison};
std::set<std::set<Compare>>possibleBetterResults = {{Compare::BETTER, Compare::BETTER},{Compare::BETTER, Compare::EQUAL}};
std::set<std::set<Compare>>possibleWorstResults = {{Compare::WORSE, Compare::WORSE},{Compare::WORSE, Compare::EQUAL}};
if(possibleBetterResults.find(comparisonResults) != possibleBetterResults.end())
return Compare::BETTER;
if(possibleWorstResults.find(comparisonResults) != possibleWorstResults.end())
return Compare::WORSE;
if (possibleBetterResults.find(comparisonResults) != possibleBetterResults.end())
return Compare::BETTER;
if (possibleWorstResults.find(comparisonResults) != possibleWorstResults.end())
return Compare::WORSE;
return Compare::DIFFERENT;
return Compare::DIFFERENT;
}
SpellTargetEvaluator::Compare SpellTargetEvaluator::compareAffectedStacksSubset(spells::Mechanics * spellMechanics, std::set<const CStack *> newSubset, std::set<const CStack *> oldSubset)
SpellTargetEvaluator::Compare SpellTargetEvaluator::compareAffectedStacksSubset(
const spells::Mechanics * spellMechanics, const std::set<const CStack *> & newSubset, const std::set<const CStack *> & oldSubset)
{
if (newSubset.size() == oldSubset.size())
return newSubset == oldSubset ? Compare::EQUAL : Compare::DIFFERENT;
if(newSubset.size() == oldSubset.size())
return newSubset == oldSubset ? Compare::EQUAL : Compare::DIFFERENT;
if (oldSubset.size() > newSubset.size())
return reverse(compareAffectedStacksSubset(spellMechanics, oldSubset, newSubset));
if(oldSubset.size() > newSubset.size())
return reverse(compareAffectedStacksSubset(spellMechanics, oldSubset, newSubset));
const std::set<const CStack *> & biggerSet = newSubset;
const std::set<const CStack *> & smallerSet = oldSubset;
const std::set<const CStack *> & biggerSet = newSubset;
const std::set<const CStack *> & smallerSet = oldSubset;
if (std::includes(biggerSet.begin(), biggerSet.end(),
smallerSet.begin(), smallerSet.end()))
return Compare::BETTER;
else
return Compare::DIFFERENT;
if(std::includes(biggerSet.begin(), biggerSet.end(), smallerSet.begin(), smallerSet.end()))
return Compare::BETTER;
else
return Compare::DIFFERENT;
}
SpellTargetEvaluator::Compare SpellTargetEvaluator::reverse(SpellTargetEvaluator::Compare compare)
{
switch(compare)
{
case Compare::BETTER:
return Compare::WORSE;
case Compare::WORSE:
return Compare::BETTER;
default:
return compare;
}
switch(compare)
{
case Compare::BETTER:
return Compare::WORSE;
case Compare::WORSE:
return Compare::BETTER;
default:
return compare;
}
}
bool SpellTargetEvaluator::canBeCastAt(spells::Mechanics * spellMechanics, BattleHex hex)
bool SpellTargetEvaluator::canBeCastAt(const spells::Mechanics * spellMechanics, BattleHex hex)
{
detail::ProblemImpl ignored;
Destination des(hex);
return spellMechanics->canBeCastAt({des}, ignored);
detail::ProblemImpl ignored;
Destination des(hex);
return spellMechanics->canBeCastAt({des}, ignored);
}
void SpellTargetEvaluator::addIfCanBeCast(spells::Mechanics * spellMechanics, BattleHex hex, std::vector<Target> & targets)
void SpellTargetEvaluator::addIfCanBeCast(const spells::Mechanics * spellMechanics, BattleHex hex, std::vector<Target> & targets)
{
detail::ProblemImpl ignored;
Destination des(hex);
if(spellMechanics->canBeCastAt({des}, ignored))
targets.push_back({des});
detail::ProblemImpl ignored;
Destination des(hex);
if(spellMechanics->canBeCastAt({des}, ignored))
targets.push_back({des});
}
+21 -18
View File
@@ -9,29 +9,32 @@
*/
#pragma once
#include <vcmi/spells/Magic.h>
#include "../../lib/spells/BattleSpellMechanics.h"
#include <vcmi/spells/Magic.h>
class SpellTargetEvaluator
{
public:
static std::vector<spells::Target> getViableTargets(spells::Mechanics * spellMechanics);
static std::vector<spells::Target> getViableTargets(const spells::Mechanics * spellMechanics);
private:
enum Compare
{
EQUAL,
DIFFERENT,
BETTER,
WORSE
};
enum Compare {
EQUAL,
DIFFERENT,
BETTER,
WORSE
};
static std::vector<spells::Target> defaultLocationSpellHeuristics(spells::Mechanics * spellMechanics);
static std::vector<spells::Target> allTargetableCreatures(spells::Mechanics * spellMechanics);
static std::vector<spells::Target> theBestLocationCasts(spells::Mechanics * spellMechanics);
static Compare compareAffectedStacks(spells::Mechanics * spellMechanics, std::set<const CStack *> newCast, std::set<const CStack *> oldCast);
static Compare compareAffectedStacksSubset(spells::Mechanics * spellMechanics, std::set<const CStack *> newSubset, std::set<const CStack *> oldSubset);
static SpellTargetEvaluator::Compare reverse(Compare compare);
static bool isCastHarmful(spells::Mechanics * spellMechanics, std::set<const CStack *> affectedStacks);
static bool canBeCastAt(spells::Mechanics * spellMechanics, BattleHex hex);
static void addIfCanBeCast(spells::Mechanics * spellMechanics, BattleHex hex, std::vector<spells::Target> & targets);
static std::vector<spells::Target> defaultLocationSpellHeuristics(const spells::Mechanics * spellMechanics);
static std::vector<spells::Target> allTargetableCreatures(const spells::Mechanics * spellMechanics);
static std::vector<spells::Target> theBestLocationCasts(const spells::Mechanics * spellMechanics);
static Compare compareAffectedStacks(
const spells::Mechanics * spellMechanics, const std::set<const CStack *> & newCast, const std::set<const CStack *> & oldCast);
static Compare compareAffectedStacksSubset(
const spells::Mechanics * spellMechanics, const std::set<const CStack *> & newSubset, const std::set<const CStack *> & oldSubset);
static SpellTargetEvaluator::Compare reverse(Compare compare);
static bool isCastHarmful(const spells::Mechanics * spellMechanics, const std::set<const CStack *> & affectedStacks);
static bool canBeCastAt(const spells::Mechanics * spellMechanics, BattleHex hex);
static void addIfCanBeCast(const spells::Mechanics * spellMechanics, BattleHex hex, std::vector<spells::Target> & targets);
};
+11 -3
View File
@@ -142,6 +142,17 @@ if(ENABLE_ERM)
)
endif()
if(ENABLE_BATTLE_AI)
file(GLOB_RECURSE BATTLE_AI_TEST_SRCS "../AI/BattleAI/*.cpp")
list(FILTER BATTLE_AI_TEST_SRCS EXCLUDE REGEX ".*main\\.cpp$")
list(APPEND test_SRCS
${BATTLE_AI_TEST_SRCS}
battleAI/SpellTargetsEvaluatorTest.cpp
)
endif()
assign_source_group(${test_SRCS} ${test_HEADERS})
set(mock_HEADERS
@@ -187,9 +198,6 @@ endif()
if(ENABLE_NULLKILLER2_AI)
target_link_libraries(vcmitest PUBLIC Nullkiller2)
endif()
if(ENABLE_BATTLE_AI)
target_link_libraries(vcmitest PRIVATE BattleAI)
endif()
target_include_directories(vcmitest
PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}
+137 -153
View File
@@ -9,13 +9,12 @@
*/
#include "StdInc.h"
#include "../mock/mock_spells_Mechanics.h"
#include "../mock/mock_battle_Unit.h"
#include "../mock/BattleFake.h"
#include "../mock/mock_battle_Unit.h"
#include "../mock/mock_spells_Mechanics.h"
#include "AI/BattleAI/SpellTargetsEvaluator.h"
#include "lib/battle/CBattleInfoCallback.h"
#include "lib/CStack.h"
#include "lib/battle/CBattleInfoCallback.h"
namespace test
{
@@ -27,206 +26,191 @@ using PossiblePositions = std::vector<BattleHex>;
class CBattleInfoCallbackMock : public CBattleInfoCallback
{
public:
MOCK_CONST_METHOD1(battleGetAllUnits, battle::Units(bool));
MOCK_CONST_METHOD0(getBattle, IBattleInfo*());
MOCK_CONST_METHOD0(getPlayerID, std::optional<PlayerColor>());
MOCK_CONST_METHOD1(battleGetAllUnits, battle::Units(bool));
MOCK_CONST_METHOD0(getBattle, IBattleInfo *());
MOCK_CONST_METHOD0(getPlayerID, std::optional<PlayerColor>());
#if SCRIPTING_ENABLED
MOCK_CONST_METHOD0(getContextPool, scripting::Pool*());
MOCK_CONST_METHOD0(getContextPool, scripting::Pool *());
#endif
};
class CStackMock : public CStack {
class CStackMock : public CStack
{
public:
MOCK_CONST_METHOD0(unitSide, BattleSide());
MOCK_CONST_METHOD0(unitSide, BattleSide());
};
class SpellTargetEvaluatorTest : public ::testing::Test
{
public:
MechanicsMock mechMock;
CBattleInfoCallbackMock battleMock;
battle::Units allUnits;
TStacks allStacks;
BattleSide casterSide = BattleSide::ATTACKER;
BattleSide enemySide = BattleSide::DEFENDER;
MechanicsMock mechMock;
CBattleInfoCallbackMock battleMock;
battle::Units allUnits;
TStacks allStacks;
BattleSide casterSide = BattleSide::ATTACKER;
BattleSide enemySide = BattleSide::DEFENDER;
void SetUp() override
{
mechMock.casterSide = casterSide;
ON_CALL(mechMock, battle()).WillByDefault(Return(&battleMock));
ON_CALL(mechMock, canBeCastAt(_, _)).WillByDefault(Return(true));
}
void SetUp() override
{
mechMock.casterSide = casterSide;
ON_CALL(mechMock, battle()).WillByDefault(Return(&battleMock));
ON_CALL(mechMock, canBeCastAt(_,_)).WillByDefault(Return(true));
}
void TearDown() override
{
for(const auto * unit : allUnits)
delete unit;
allUnits.clear();
for(const auto * stack : allStacks)
delete stack;
allStacks.clear();
}
void TearDown() override
{
for (const auto * unit : allUnits)
delete unit;
allUnits.clear();
for (const auto * stack : allStacks)
delete stack;
allStacks.clear();
}
void spellTargetTypes(std::vector<AimType> targetTypes)
{
ON_CALL(mechMock, getTargetTypes()).WillByDefault(Return(std::move(targetTypes)));
}
void spellTargetTypes(std::vector<AimType> targetTypes)
{
ON_CALL(mechMock, getTargetTypes()).WillByDefault(Return(std::move(targetTypes)));
}
CStackMock * addStack(BattleHex position, BattleSide battleSide, bool isSuspectible = true)
{
auto * stack = new CStackMock();
ON_CALL(*stack, unitSide()).WillByDefault(Return(battleSide));
allStacks.push_back(stack);
auto * unit = new UnitMock();
ON_CALL(*unit, getPosition()).WillByDefault(Return(position));
allUnits.push_back(unit);
CStackMock * addStack(BattleHex position, BattleSide battleSide, bool isSuspectible = true)
{
auto * stack = new CStackMock();
ON_CALL(*stack, unitSide()).WillByDefault(Return(battleSide));
allStacks.push_back(stack);
auto * unit = new UnitMock();
ON_CALL(*unit, getPosition()).WillByDefault(Return(position));
allUnits.push_back(unit);
if(!isSuspectible)
ON_CALL(mechMock, canBeCastAt(Contains(Field(&Destination::hexValue, position)), _)).WillByDefault(Return(false));
return stack;
}
if (!isSuspectible)
ON_CALL(mechMock, canBeCastAt(Contains(Field(&Destination::hexValue, position)),_)).WillByDefault(Return(false));
return stack;
}
void setAffectedStacksForCast(BattleHex position, std::vector<const CStack *> stacks)
{
ON_CALL(mechMock, getAffectedStacks(Contains(Field(&Destination::hexValue, position)))).WillByDefault(Return(stacks));
}
void setAffectedStacksForCast(BattleHex position, std::vector<const CStack *> stacks)
{
ON_CALL(mechMock, getAffectedStacks(Contains(Field(&Destination::hexValue, position)))).WillByDefault(Return(stacks));
}
void confirmResults(std::vector<PossiblePositions> allRequiredCasts)
{
std::vector<Target> result = SpellTargetEvaluator::getViableTargets(&mechMock);
basicCheck(result);
ASSERT_EQ(result.size(), allRequiredCasts.size());
void confirmResults(std::vector<PossiblePositions> allRequiredCasts)
{
std::vector<Target> result = SpellTargetEvaluator::getViableTargets(&mechMock);
basicCheck(result);
ASSERT_EQ(result.size(), allRequiredCasts.size());
std::vector<BattleHex> targetedHexes;
targetedHexes.reserve(result.size());
for(Target target : result)
targetedHexes.push_back(target.front().hexValue);
std::vector<BattleHex> targetedHexes;
targetedHexes.reserve(result.size());
for (Target target : result)
targetedHexes.push_back(target.front().hexValue);
for(const PossiblePositions & requiredCast : allRequiredCasts)
EXPECT_TRUE(containCommonValue(requiredCast, targetedHexes));
}
for (const PossiblePositions & requiredCast : allRequiredCasts)
EXPECT_TRUE(containCommonValue(requiredCast, targetedHexes));
}
void basicCheck(std::vector<Target> & result)
{
for (const Target & target : result)
EXPECT_EQ(target.size(), 1); //multi-destination spells are not handled by targetEvaluator
}
template<typename T>
bool containCommonValue(const std::vector<T> & v1, const std::vector<T> & v2)
{
for (T val1 : v1)
{
for (T val2 : v2)
{
if (val1 == val2)
return true;
}
}
return false;
}
void basicCheck(std::vector<Target> & result)
{
for(const Target & target : result)
EXPECT_EQ(target.size(), 1); //multi-destination spells are not handled by targetEvaluator
}
template<typename T>
bool containCommonValue(const std::vector<T> & v1, const std::vector<T> & v2)
{
for(T val1 : v1)
{
for(T val2 : v2)
{
if(val1 == val2)
return true;
}
}
return false;
}
};
TEST_F(SpellTargetEvaluatorTest, ReturnsEmptyIfMechanicsNullptr)
{
spellTargetTypes({AimType::CREATURE});
std::vector<Target> result = SpellTargetEvaluator::getViableTargets(nullptr);
EXPECT_TRUE(result.empty());
}
TEST_F(SpellTargetEvaluatorTest, ReturnsEmptyIfMultiDestinationSpell)
{
spellTargetTypes({AimType::CREATURE, AimType::LOCATION});
std::vector<Target> result = SpellTargetEvaluator::getViableTargets(&mechMock);
EXPECT_TRUE(result.empty());
spellTargetTypes({AimType::CREATURE, AimType::LOCATION});
std::vector<Target> result = SpellTargetEvaluator::getViableTargets(&mechMock);
EXPECT_TRUE(result.empty());
}
TEST_F(SpellTargetEvaluatorTest, ReturnSingleEmptyDestinationIfTargetIsNone)
{
spellTargetTypes({AimType::NO_TARGET});
std::vector<Target> result = SpellTargetEvaluator::getViableTargets(&mechMock);
EXPECT_EQ(result.size(), 1);
EXPECT_TRUE(result.front().empty());
spellTargetTypes({AimType::NO_TARGET});
std::vector<Target> result = SpellTargetEvaluator::getViableTargets(&mechMock);
EXPECT_EQ(result.size(), 1);
EXPECT_TRUE(result.front().empty());
}
TEST_F(SpellTargetEvaluatorTest, ReturnsSuspectibleCreaturePositionsIfSpellTargetsCreatures)
{
spellTargetTypes({AimType::CREATURE});
spellTargetTypes({AimType::CREATURE});
addStack(BattleHex(1), casterSide);
addStack(BattleHex(2), enemySide, false);
addStack(BattleHex(3), casterSide);
ON_CALL(battleMock, battleGetAllUnits(Eq(false))).WillByDefault(Return(allUnits));
addStack(BattleHex(1), casterSide);
addStack(BattleHex(2), enemySide, false);
addStack(BattleHex(3), casterSide);
ON_CALL(battleMock, battleGetAllUnits(Eq(false))).WillByDefault(Return(allUnits));
confirmResults({{BattleHex(1)}, {BattleHex(3)}});
confirmResults({{BattleHex(1)}, {BattleHex(3)}});
}
TEST_F(SpellTargetEvaluatorTest, ReturnsSuspectibleCreaturePositionsAndSingleRandomSurroundingHexForEachStackIfNeutralLocationSpell)
{
spellTargetTypes({AimType::LOCATION});
ON_CALL(mechMock, isNeutralSpell()).WillByDefault(Return(true));
spellTargetTypes({AimType::LOCATION});
ON_CALL(mechMock, isNeutralSpell()).WillByDefault(Return(true));
addStack(BattleHex(72), casterSide);
addStack(BattleHex(159), enemySide, false);
addStack(BattleHex(23), casterSide);
ON_CALL(battleMock, battleGetAllUnits(Eq(false))).WillByDefault(Return(allUnits));
addStack(BattleHex(72), casterSide);
addStack(BattleHex(159), enemySide, false);
addStack(BattleHex(23), casterSide);
ON_CALL(battleMock, battleGetAllUnits(Eq(false))).WillByDefault(Return(allUnits));
confirmResults(
{
{BattleHex(72)}, BattleHex(72).getAllNeighbouringTiles().toVector(),
BattleHex(159).getAllNeighbouringTiles().toVector(),
{BattleHex(23)}, BattleHex(23).getAllNeighbouringTiles().toVector()
});
confirmResults(
{{BattleHex(72)},
BattleHex(72).getAllNeighbouringTiles().toVector(),
BattleHex(159).getAllNeighbouringTiles().toVector(),
{BattleHex(23)},
BattleHex(23).getAllNeighbouringTiles().toVector()}
);
}
TEST_F(SpellTargetEvaluatorTest, ReturnsOneCaseOfEachOptimalCastIfNegativeLocationSpell)
{
spellTargetTypes({AimType::LOCATION});
ON_CALL(mechMock, isNegativeSpell()).WillByDefault(Return(true));
spellTargetTypes({AimType::LOCATION});
ON_CALL(mechMock, isNegativeSpell()).WillByDefault(Return(true));
auto *enemyStack1 = addStack(BattleHex(90), enemySide);
auto *enemyStack2 = addStack(BattleHex(106), enemySide);
auto *enemyStack3 = addStack(BattleHex(1), enemySide);
auto *enemyStack4 = addStack(BattleHex(37), enemySide);
auto *enemyStack5 = addStack(BattleHex(41), enemySide);
auto * enemyStack1 = addStack(BattleHex(90), enemySide);
auto * enemyStack2 = addStack(BattleHex(106), enemySide);
auto * enemyStack3 = addStack(BattleHex(1), enemySide);
auto * enemyStack4 = addStack(BattleHex(37), enemySide);
auto * enemyStack5 = addStack(BattleHex(41), enemySide);
auto *alliedStack1 = addStack(BattleHex(107), casterSide);
auto *alliedStack2 = addStack(BattleHex(19), casterSide);
auto * alliedStack1 = addStack(BattleHex(107), casterSide);
auto * alliedStack2 = addStack(BattleHex(19), casterSide);
//optimal
setAffectedStacksForCast(BattleHex(71), {enemyStack1, enemyStack2});
setAffectedStacksForCast(BattleHex(88), {enemyStack1, enemyStack2});
//suboptimal
setAffectedStacksForCast(BattleHex(55), {enemyStack1});
setAffectedStacksForCast(BattleHex(89), {enemyStack1, enemyStack2, alliedStack1});
//optimal
setAffectedStacksForCast(BattleHex(71), {enemyStack1, enemyStack2});
setAffectedStacksForCast(BattleHex(88), {enemyStack1, enemyStack2});
//suboptimal
setAffectedStacksForCast(BattleHex(55), {enemyStack1});
setAffectedStacksForCast(BattleHex(89), {enemyStack1, enemyStack2, alliedStack1});
//optimal
setAffectedStacksForCast(BattleHex(18), {enemyStack3, alliedStack2});
setAffectedStacksForCast(BattleHex(2), {enemyStack3, alliedStack2});
//suboptimal
setAffectedStacksForCast(BattleHex(53), {alliedStack2});
//optimal
setAffectedStacksForCast(BattleHex(18), {enemyStack3, alliedStack2});
setAffectedStacksForCast(BattleHex(2), {enemyStack3, alliedStack2});
//suboptimal
setAffectedStacksForCast(BattleHex(53), {alliedStack2});
//optimal
setAffectedStacksForCast(BattleHex(39), {enemyStack4, enemyStack5});
//suboptimal
setAffectedStacksForCast(BattleHex(21), {enemyStack4});
setAffectedStacksForCast(BattleHex(25), {enemyStack5});
//optimal
setAffectedStacksForCast(BattleHex(39), {enemyStack4, enemyStack5});
//suboptimal
setAffectedStacksForCast(BattleHex(21), {enemyStack4});
setAffectedStacksForCast(BattleHex(25), {enemyStack5});
confirmResults(
{
{BattleHex(71), BattleHex(88)},
{BattleHex(2), BattleHex(18)},
{BattleHex(39)}
});
confirmResults({
{BattleHex(71), BattleHex(88)},
{BattleHex(2), BattleHex(18)},
{BattleHex(39)}
});
}
}