2021-05-15 19:22:44 +03:00
/*
* FuzzyEngines.cpp, part of VCMI engine
*
* Authors: listed in file AUTHORS in main folder
*
* License: GNU General Public License v2.0 or later
* Full text of license available in license.txt file, in main folder
*
*/
#include "StdInc.h"
#include "FuzzyEngines.h"
#include "Goals/Goals.h"
#include "../../lib/mapObjects/MapObjects.h"
#include "VCAI.h"
#include "MapObjectsEvaluator.h"
#define MIN_AI_STRENGTH (0.5f) //lower when combat AI gets smarter
#define UNGUARDED_OBJECT (100.0f) //we consider unguarded objects 100 times weaker than us
extern boost :: thread_specific_ptr < VCAI > ai ;
extern FuzzyHelper * fh ;
engineBase :: engineBase ()
{
engine . addRuleBlock ( & rules );
}
void engineBase :: configure ()
{
engine . configure ( "Minimum" , "Maximum" , "Minimum" , "AlgebraicSum" , "Centroid" , "Proportional" );
logAi -> info ( engine . toString ());
}
void engineBase :: addRule ( const std :: string & txt )
{
rules . addRule ( fl :: Rule :: parse ( txt , & engine ));
}
struct armyStructure
{
float walkers , shooters , flyers ;
ui32 maxSpeed ;
};
armyStructure evaluateArmyStructure ( const CArmedInstance * army )
{
ui64 totalStrenght = army -> getArmyStrength ();
double walkersStrenght = 0 ;
double flyersStrenght = 0 ;
double shootersStrenght = 0 ;
ui32 maxSpeed = 0 ;
static const CSelector selectorSHOOTER = Selector :: type ( Bonus :: SHOOTER );
static const std :: string keySHOOTER = "type_" + std :: to_string (( int32_t ) Bonus :: SHOOTER );
static const CSelector selectorFLYING = Selector :: type ( Bonus :: FLYING );
static const std :: string keyFLYING = "type_" + std :: to_string (( int32_t ) Bonus :: FLYING );
static const CSelector selectorSTACKS_SPEED = Selector :: type ( Bonus :: STACKS_SPEED );
static const std :: string keySTACKS_SPEED = "type_" + std :: to_string (( int32_t ) Bonus :: STACKS_SPEED );
for ( auto s : army -> Slots ())
{
bool walker = true ;
const CCreature * creature = s . second -> type ;
if ( creature -> hasBonus ( selectorSHOOTER , keySHOOTER ))
{
shootersStrenght += s . second -> getPower ();
walker = false ;
}
if ( creature -> hasBonus ( selectorFLYING , keyFLYING ))
{
flyersStrenght += s . second -> getPower ();
walker = false ;
}
if ( walker )
walkersStrenght += s . second -> getPower ();
vstd :: amax ( maxSpeed , creature -> valOfBonuses ( selectorSTACKS_SPEED , keySTACKS_SPEED ));
}
armyStructure as ;
as . walkers = walkersStrenght / totalStrenght ;
as . shooters = shootersStrenght / totalStrenght ;
as . flyers = flyersStrenght / totalStrenght ;
as . maxSpeed = maxSpeed ;
assert ( as . walkers || as . flyers || as . shooters );
return as ;
}
float HeroMovementGoalEngineBase :: calculateTurnDistanceInputValue ( const Goals :: AbstractGoal & goal ) const
{
if ( goal . evaluationContext . movementCost > 0 )
{
return goal . evaluationContext . movementCost ;
}
else
{
auto pathInfo = ai -> myCb -> getPathsInfo ( goal . hero . h ) -> getPathInfo ( goal . tile );
return pathInfo -> cost ;
}
}
TacticalAdvantageEngine :: TacticalAdvantageEngine ()
{
try
{
ourShooters = new fl :: InputVariable ( "OurShooters" );
ourWalkers = new fl :: InputVariable ( "OurWalkers" );
ourFlyers = new fl :: InputVariable ( "OurFlyers" );
enemyShooters = new fl :: InputVariable ( "EnemyShooters" );
enemyWalkers = new fl :: InputVariable ( "EnemyWalkers" );
enemyFlyers = new fl :: InputVariable ( "EnemyFlyers" );
//Tactical advantage calculation
std :: vector < fl :: InputVariable *> helper =
{
ourShooters , ourWalkers , ourFlyers , enemyShooters , enemyWalkers , enemyFlyers
};
for ( auto val : helper )
{
engine . addInputVariable ( val );
val -> addTerm ( new fl :: Ramp ( "FEW" , 0.6 , 0.0 ));
val -> addTerm ( new fl :: Ramp ( "MANY" , 0.4 , 1 ));
val -> setRange ( 0.0 , 1.0 );
}
ourSpeed = new fl :: InputVariable ( "OurSpeed" );
enemySpeed = new fl :: InputVariable ( "EnemySpeed" );
helper = { ourSpeed , enemySpeed };
for ( auto val : helper )
{
engine . addInputVariable ( val );
val -> addTerm ( new fl :: Ramp ( "LOW" , 6.5 , 3 ));
val -> addTerm ( new fl :: Triangle ( "MEDIUM" , 5.5 , 10.5 ));
val -> addTerm ( new fl :: Ramp ( "HIGH" , 8.5 , 16 ));
val -> setRange ( 0 , 25 );
}
castleWalls = new fl :: InputVariable ( "CastleWalls" );
engine . addInputVariable ( castleWalls );
{
fl :: Rectangle * none = new fl :: Rectangle ( "NONE" , CGTownInstance :: NONE , CGTownInstance :: NONE + ( CGTownInstance :: FORT - CGTownInstance :: NONE ) * 0.5f );
castleWalls -> addTerm ( none );
fl :: Trapezoid * medium = new fl :: Trapezoid ( "MEDIUM" , ( CGTownInstance :: FORT - CGTownInstance :: NONE ) * 0.5f , CGTownInstance :: FORT ,
CGTownInstance :: CITADEL , CGTownInstance :: CITADEL + ( CGTownInstance :: CASTLE - CGTownInstance :: CITADEL ) * 0.5f );
castleWalls -> addTerm ( medium );
fl :: Ramp * high = new fl :: Ramp ( "HIGH" , CGTownInstance :: CITADEL - 0.1 , CGTownInstance :: CASTLE );
castleWalls -> addTerm ( high );
castleWalls -> setRange ( CGTownInstance :: NONE , CGTownInstance :: CASTLE );
}
bankPresent = new fl :: InputVariable ( "Bank" );
engine . addInputVariable ( bankPresent );
{
fl :: Rectangle * termFalse = new fl :: Rectangle ( "FALSE" , 0.0 , 0.5f );
bankPresent -> addTerm ( termFalse );
fl :: Rectangle * termTrue = new fl :: Rectangle ( "TRUE" , 0.5f , 1 );
bankPresent -> addTerm ( termTrue );
bankPresent -> setRange ( 0 , 1 );
}
threat = new fl :: OutputVariable ( "Threat" );
engine . addOutputVariable ( threat );
threat -> addTerm ( new fl :: Ramp ( "LOW" , 1 , MIN_AI_STRENGTH ));
threat -> addTerm ( new fl :: Triangle ( "MEDIUM" , 0.8 , 1.2 ));
threat -> addTerm ( new fl :: Ramp ( "HIGH" , 1 , 1.5 ));
threat -> setRange ( MIN_AI_STRENGTH , 1.5 );
addRule ( "if OurShooters is MANY and EnemySpeed is LOW then Threat is LOW" );
addRule ( "if OurShooters is MANY and EnemyShooters is FEW then Threat is LOW" );
addRule ( "if OurSpeed is LOW and EnemyShooters is MANY then Threat is HIGH" );
addRule ( "if OurSpeed is HIGH and EnemyShooters is MANY then Threat is LOW" );
addRule ( "if OurWalkers is FEW and EnemyShooters is MANY then Threat is LOW" );
addRule ( "if OurShooters is MANY and EnemySpeed is HIGH then Threat is HIGH" );
//just to cover all cases
addRule ( "if OurShooters is FEW and EnemySpeed is HIGH then Threat is MEDIUM" );
addRule ( "if EnemySpeed is MEDIUM then Threat is MEDIUM" );
addRule ( "if EnemySpeed is LOW and OurShooters is FEW then Threat is MEDIUM" );
addRule ( "if Bank is TRUE and OurShooters is MANY then Threat is HIGH" );
addRule ( "if Bank is TRUE and EnemyShooters is MANY then Threat is LOW" );
addRule ( "if CastleWalls is HIGH and OurWalkers is MANY then Threat is HIGH" );
addRule ( "if CastleWalls is HIGH and OurFlyers is MANY and OurShooters is MANY then Threat is MEDIUM" );
addRule ( "if CastleWalls is MEDIUM and OurShooters is MANY and EnemyWalkers is MANY then Threat is LOW" );
}
catch ( fl :: Exception & pe )
{
logAi -> error ( "initTacticalAdvantage: %s" , pe . getWhat ());
}
configure ();
}
float TacticalAdvantageEngine :: getTacticalAdvantage ( const CArmedInstance * we , const CArmedInstance * enemy )
{
float output = 1 ;
/*try //TODO: rework this engine, it tends to produce nonsense output
{
armyStructure ourStructure = evaluateArmyStructure(we);
armyStructure enemyStructure = evaluateArmyStructure(enemy);
ourWalkers->setValue(ourStructure.walkers);
ourShooters->setValue(ourStructure.shooters);
ourFlyers->setValue(ourStructure.flyers);
ourSpeed->setValue(ourStructure.maxSpeed);
enemyWalkers->setValue(enemyStructure.walkers);
enemyShooters->setValue(enemyStructure.shooters);
enemyFlyers->setValue(enemyStructure.flyers);
enemySpeed->setValue(enemyStructure.maxSpeed);
bool bank = dynamic_cast<const CBank *>(enemy);
if(bank)
bankPresent->setValue(1);
else
bankPresent->setValue(0);
const CGTownInstance * fort = dynamic_cast<const CGTownInstance *>(enemy);
if(fort)
castleWalls->setValue(fort->fortLevel());
else
castleWalls->setValue(0);
engine.process();
output = threat->getValue();
}
catch(fl::Exception & fe)
{
logAi->error("getTacticalAdvantage: %s ", fe.getWhat());
}
if(output < 0 || (output != output))
{
fl::InputVariable * tab[] = { bankPresent, castleWalls, ourWalkers, ourShooters, ourFlyers, ourSpeed, enemyWalkers, enemyShooters, enemyFlyers, enemySpeed };
std::string names[] = { "bankPresent", "castleWalls", "ourWalkers", "ourShooters", "ourFlyers", "ourSpeed", "enemyWalkers", "enemyShooters", "enemyFlyers", "enemySpeed" };
std::stringstream log("Warning! Fuzzy engine doesn't cover this set of parameters: ");
for(int i = 0; i < boost::size(tab); i++)
log << names[i] << ": " << tab[i]->getValue() << " ";
logAi->error(log.str());
assert(false);
}*/
return output ;
}
//std::shared_ptr<AbstractGoal> chooseSolution (std::vector<std::shared_ptr<AbstractGoal>> & vec)
HeroMovementGoalEngineBase :: HeroMovementGoalEngineBase ()
{
try
{
strengthRatio = new fl :: InputVariable ( "strengthRatio" ); //hero must be strong enough to defeat guards
2021-05-15 19:23:38 +03:00
heroStrengthVariable = new fl :: InputVariable ( "heroStrengthVariable" ); //we want to use weakest possible hero
turnDistanceVariable = new fl :: InputVariable ( "turnDistanceVariable" ); //we want to use hero who is near
2021-05-15 19:22:44 +03:00
missionImportance = new fl :: InputVariable ( "lockedMissionImportance" ); //we may want to preempt hero with low-priority mission
value = new fl :: OutputVariable ( "Value" );
value -> setMinimum ( 0 );
value -> setMaximum ( 5 );
2021-05-15 19:23:38 +03:00
std :: vector < fl :: InputVariable *> helper = { strengthRatio , heroStrengthVariable , turnDistanceVariable , missionImportance };
2021-05-15 19:22:44 +03:00
for ( auto val : helper )
{
engine . addInputVariable ( val );
}
engine . addOutputVariable ( value );
strengthRatio -> addTerm ( new fl :: Ramp ( "LOW" , SAFE_ATTACK_CONSTANT , 0 ));
strengthRatio -> addTerm ( new fl :: Ramp ( "HIGH" , SAFE_ATTACK_CONSTANT , SAFE_ATTACK_CONSTANT * 3 ));
strengthRatio -> setRange ( 0 , SAFE_ATTACK_CONSTANT * 3 );
//strength compared to our main hero
2021-05-15 19:23:38 +03:00
heroStrengthVariable -> addTerm ( new fl :: Ramp ( "LOW" , 0.5 , 0 ));
heroStrengthVariable -> addTerm ( new fl :: Triangle ( "MEDIUM" , 0.2 , 0.8 ));
heroStrengthVariable -> addTerm ( new fl :: Ramp ( "HIGH" , 0.5 , 1 ));
heroStrengthVariable -> setRange ( 0.0 , 1.0 );
2021-05-15 19:22:44 +03:00
2021-05-15 19:23:38 +03:00
turnDistanceVariable -> addTerm ( new fl :: Ramp ( "SHORT" , 0.5 , 0 ));
turnDistanceVariable -> addTerm ( new fl :: Triangle ( "MEDIUM" , 0.1 , 0.8 ));
turnDistanceVariable -> addTerm ( new fl :: Ramp ( "LONG" , 0.5 , 10 ));
turnDistanceVariable -> setRange ( 0.0 , 10.0 );
2021-05-15 19:22:44 +03:00
missionImportance -> addTerm ( new fl :: Ramp ( "LOW" , 2.5 , 0 ));
missionImportance -> addTerm ( new fl :: Triangle ( "MEDIUM" , 2 , 3 ));
missionImportance -> addTerm ( new fl :: Ramp ( "HIGH" , 2.5 , 5 ));
missionImportance -> setRange ( 0.0 , 5.0 );
//an issue: in 99% cases this outputs center of mass (2.5) regardless of actual input :/
//should be same as "mission Importance" to keep consistency
value -> addTerm ( new fl :: Ramp ( "LOW" , 2.5 , 0 ));
value -> addTerm ( new fl :: Triangle ( "MEDIUM" , 2 , 3 )); //can't be center of mass :/
value -> addTerm ( new fl :: Ramp ( "HIGH" , 2.5 , 5 ));
value -> setRange ( 0.0 , 5.0 );
//use unarmed scouts if possible
2021-05-15 19:23:38 +03:00
addRule ( "if strengthRatio is HIGH and heroStrengthVariable is LOW then Value is HIGH" );
2021-05-15 19:22:44 +03:00
//we may want to use secondary hero(es) rather than main hero
2021-05-15 19:23:38 +03:00
addRule ( "if strengthRatio is HIGH and heroStrengthVariable is MEDIUM then Value is MEDIUM" );
addRule ( "if strengthRatio is HIGH and heroStrengthVariable is HIGH then Value is LOW" );
2021-05-15 19:22:44 +03:00
//don't assign targets to heroes who are too weak, but prefer targets of our main hero (in case we need to gather army)
2021-05-15 19:23:38 +03:00
addRule ( "if strengthRatio is LOW and heroStrengthVariable is LOW then Value is LOW" );
2021-05-15 19:22:44 +03:00
//attempt to arm secondary heroes is not stupid
2021-05-15 19:23:38 +03:00
addRule ( "if strengthRatio is LOW and heroStrengthVariable is MEDIUM then Value is HIGH" );
addRule ( "if strengthRatio is LOW and heroStrengthVariable is HIGH then Value is LOW" );
2021-05-15 19:22:44 +03:00
//do not cancel important goals
addRule ( "if lockedMissionImportance is HIGH then Value is LOW" );
addRule ( "if lockedMissionImportance is MEDIUM then Value is MEDIUM" );
addRule ( "if lockedMissionImportance is LOW then Value is HIGH" );
//pick nearby objects if it's easy, avoid long walks
2021-05-15 19:23:38 +03:00
addRule ( "if turnDistanceVariable is SHORT then Value is HIGH" );
addRule ( "if turnDistanceVariable is MEDIUM then Value is MEDIUM" );
addRule ( "if turnDistanceVariable is LONG then Value is LOW" );
2021-05-15 19:22:44 +03:00
}
catch ( fl :: Exception & fe )
{
logAi -> error ( "HeroMovementGoalEngineBase: %s" , fe . getWhat ());
}
}
void HeroMovementGoalEngineBase :: setSharedFuzzyVariables ( Goals :: AbstractGoal & goal )
{
float turns = calculateTurnDistanceInputValue ( goal );
float missionImportanceData = 0 ;
if ( vstd :: contains ( ai -> lockedHeroes , goal . hero ))
{
missionImportanceData = ai -> lockedHeroes [ goal . hero ] -> priority ;
}
else if ( goal . parent )
{
missionImportanceData = goal . parent -> priority ;
}
float strengthRatioData = 10.0f ; //we are much stronger than enemy
ui64 danger = fh -> evaluateDanger ( goal . tile , goal . hero . h );
if ( danger )
strengthRatioData = ( fl :: scalar ) goal . hero . h -> getTotalStrength () / danger ;
try
{
strengthRatio -> setValue ( strengthRatioData );
2021-05-15 19:23:38 +03:00
heroStrengthVariable -> setValue (( fl :: scalar ) goal . hero -> getTotalStrength () / ai -> primaryHero () -> getTotalStrength ());
turnDistanceVariable -> setValue ( turns );
2021-05-15 19:22:44 +03:00
missionImportance -> setValue ( missionImportanceData );
}
catch ( fl :: Exception & fe )
{
logAi -> error ( "HeroMovementGoalEngineBase::setSharedFuzzyVariables: %s" , fe . getWhat ());
}
}
VisitObjEngine :: VisitObjEngine ()
{
try
{
objectValue = new fl :: InputVariable ( "objectValue" ); //value of that object type known by AI
engine . addInputVariable ( objectValue );
//objectValue ranges are based on checking RMG priorities of some objects and checking LOW/MID/HIGH proportions for various values in QtFuzzyLite
objectValue -> addTerm ( new fl :: Ramp ( "LOW" , 3500.0 , 0.0 ));
objectValue -> addTerm ( new fl :: Triangle ( "MEDIUM" , 0.0 , 8500.0 ));
std :: vector < fl :: Discrete :: Pair > multiRamp = { fl :: Discrete :: Pair ( 5000.0 , 0.0 ), fl :: Discrete :: Pair ( 10000.0 , 0.75 ), fl :: Discrete :: Pair ( 20000.0 , 1.0 ) };
objectValue -> addTerm ( new fl :: Discrete ( "HIGH" , multiRamp ));
objectValue -> setRange ( 0.0 , 20000.0 ); //relic artifact value is border value by design, even better things are scaled down.
addRule ( "if objectValue is HIGH then Value is HIGH" );
addRule ( "if objectValue is MEDIUM then Value is MEDIUM" );
addRule ( "if objectValue is LOW then Value is LOW" );
}
catch ( fl :: Exception & fe )
{
logAi -> error ( "FindWanderTarget: %s" , fe . getWhat ());
}
configure ();
}
float VisitObjEngine :: evaluate ( Goals :: VisitObj & goal )
{
if ( ! goal . hero )
return 0 ;
auto obj = ai -> myCb -> getObj ( ObjectInstanceID ( goal . objid ));
if ( ! obj )
{
logAi -> error ( "Goals::VisitObj objid " + std :: to_string ( goal . objid ) + " no longer visible, probably goal used for something it's not intended" );
return - 100 ; // FIXME: Added check when goal was used for hero instead of VisitHero, but crashes are bad anyway
}
boost :: optional < int > objValueKnownByAI = MapObjectsEvaluator :: getInstance (). getObjectValue ( obj );
int objValue = 0 ;
if ( objValueKnownByAI != boost :: none ) //consider adding value manipulation based on object instances on map
{
objValue = std :: min ( std :: max ( objValueKnownByAI . get (), 0 ), 20000 );
}
else
{
MapObjectsEvaluator :: getInstance (). addObjectData ( obj -> ID , obj -> subID , 0 );
logGlobal -> error ( "AI met object type it doesn't know - ID: " + std :: to_string ( obj -> ID ) + ", subID: " + std :: to_string ( obj -> subID ) + " - adding to database with value " + std :: to_string ( objValue ));
}
setSharedFuzzyVariables ( goal );
float output = - 1.0f ;
try
{
objectValue -> setValue ( objValue );
engine . process ();
output = value -> getValue ();
}
catch ( fl :: Exception & fe )
{
logAi -> error ( "evaluate getWanderTargetObjectValue: %s" , fe . getWhat ());
}
assert ( output >= 0.0f );
return output ;
}
VisitTileEngine :: VisitTileEngine () //so far no VisitTile-specific variables that are not shared with HeroMovementGoalEngineBase
{
configure ();
}
float VisitTileEngine :: evaluate ( Goals :: VisitTile & goal )
{
//we assume that hero is already set and we want to choose most suitable one for the mission
if ( ! goal . hero )
return 0 ;
//assert(cb->isInTheMap(g.tile));
setSharedFuzzyVariables ( goal );
try
{
engine . process ();
goal . priority = value -> getValue ();
}
catch ( fl :: Exception & fe )
{
logAi -> error ( "evaluate VisitTile: %s" , fe . getWhat ());
}
assert ( goal . priority >= 0 );
return goal . priority ;
}