From e33ef718829e91b460d28474facbc841fc166905 Mon Sep 17 00:00:00 2001 From: Dydzio Date: Sat, 4 Aug 2018 18:58:50 +0200 Subject: [PATCH] FuzzyHelper is on diet... gotta get thinner --- AI/VCAI/Fuzzy.cpp | 255 ++++++++++++++++++++++------------------------ AI/VCAI/Fuzzy.h | 21 ++-- 2 files changed, 134 insertions(+), 142 deletions(-) diff --git a/AI/VCAI/Fuzzy.cpp b/AI/VCAI/Fuzzy.cpp index ded588dbe..810cc20b2 100644 --- a/AI/VCAI/Fuzzy.cpp +++ b/AI/VCAI/Fuzzy.cpp @@ -202,7 +202,7 @@ void FuzzyHelper::initTacticalAdvantage() } } -float FuzzyHelper::calculateTurnDistanceInputValue(const CGHeroInstance * h, int3 tile) const +float HeroMovementGoalEngineBase::calculateTurnDistanceInputValue(const CGHeroInstance * h, int3 tile) const { float turns = 0.0f; float distance = CPathfinderHelper::getMovementCost(h, tile); @@ -311,7 +311,7 @@ float FuzzyHelper::getWanderTargetObjectValue(const CGHeroInstance & h, const Ob wanderTarget.turnDistance->setValue(distFromObject); wanderTarget.objectValue->setValue(objValue); wanderTarget.engine.process(); - output = wanderTarget.visitGain->getValue(); + output = wanderTarget.value->getValue(); } catch (fl::Exception & fe) { @@ -373,7 +373,87 @@ float FuzzyHelper::evaluate(Goals::RecruitHero & g) { return 1; } -HeroMovementGoalEngine::~HeroMovementGoalEngine() +HeroMovementGoalEngineBase::HeroMovementGoalEngineBase() +{ + try + { + strengthRatio = new fl::InputVariable("strengthRatio"); //hero must be strong enough to defeat guards + heroStrength = new fl::InputVariable("heroStrength"); //we want to use weakest possible hero + turnDistance = new fl::InputVariable("turnDistance"); //we want to use hero who is near + missionImportance = new fl::InputVariable("lockedMissionImportance"); //we may want to preempt hero with low-priority mission + estimatedReward = new fl::InputVariable("estimatedReward"); //indicate AI that content of the file is important or it is probably bad + value = new fl::OutputVariable("Value"); + value->setMinimum(0); + value->setMaximum(5); + + std::vector helper = { strengthRatio, heroStrength, turnDistance, missionImportance, estimatedReward }; + 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 + heroStrength->addTerm(new fl::Ramp("LOW", 0.2, 0)); + heroStrength->addTerm(new fl::Triangle("MEDIUM", 0.2, 0.8)); + heroStrength->addTerm(new fl::Ramp("HIGH", 0.5, 1)); + heroStrength->setRange(0.0, 1.0); + + turnDistance->addTerm(new fl::Ramp("SMALL", 0.5, 0)); + turnDistance->addTerm(new fl::Triangle("MEDIUM", 0.1, 0.8)); + turnDistance->addTerm(new fl::Ramp("LONG", 0.5, 3)); + turnDistance->setRange(0.0, 3.0); + + 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); + + estimatedReward->addTerm(new fl::Ramp("LOW", 2.5, 0)); + estimatedReward->addTerm(new fl::Ramp("HIGH", 2.5, 5)); + estimatedReward->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 + addRule("if strengthRatio is HIGH and heroStrength is LOW then Value is very HIGH"); + //we may want to use secondary hero(es) rather than main hero + addRule("if strengthRatio is HIGH and heroStrength is MEDIUM then Value is somewhat HIGH"); + addRule("if strengthRatio is HIGH and heroStrength is HIGH then Value is somewhat LOW"); + //don't assign targets to heroes who are too weak, but prefer targets of our main hero (in case we need to gather army) + addRule("if strengthRatio is LOW and heroStrength is LOW then Value is very LOW"); + //attempt to arm secondary heroes is not stupid + addRule("if strengthRatio is LOW and heroStrength is MEDIUM then Value is somewhat HIGH"); + addRule("if strengthRatio is LOW and heroStrength is HIGH then Value is LOW"); + + //do not cancel important goals + addRule("if lockedMissionImportance is HIGH then Value is very LOW"); + addRule("if lockedMissionImportance is MEDIUM then Value is somewhat LOW"); + addRule("if lockedMissionImportance is LOW then Value is HIGH"); + //pick nearby objects if it's easy, avoid long walks + addRule("if turnDistance is SMALL then Value is HIGH"); + addRule("if turnDistance is MEDIUM then Value is MEDIUM"); + addRule("if turnDistance is LONG then Value is LOW"); + //some goals are more rewarding by definition f.e. capturing town is more important than collecting resource - experimental + addRule("if estimatedReward is HIGH then Value is very HIGH"); + addRule("if estimatedReward is LOW then Value is somewhat LOW"); + } + catch(fl::Exception & fe) + { + logAi->error("HeroMovementGoalEngineBase: %s", fe.getWhat()); + } +} + +HeroMovementGoalEngineBase::~HeroMovementGoalEngineBase() { delete strengthRatio; delete heroStrength; @@ -382,135 +462,6 @@ HeroMovementGoalEngine::~HeroMovementGoalEngine() delete estimatedReward; } -void FuzzyHelper::initVisitTile() -{ - try - { - vt.strengthRatio = new fl::InputVariable("strengthRatio"); //hero must be strong enough to defeat guards - vt.heroStrength = new fl::InputVariable("heroStrength"); //we want to use weakest possible hero - vt.turnDistance = new fl::InputVariable("turnDistance"); //we want to use hero who is near - vt.missionImportance = new fl::InputVariable("lockedMissionImportance"); //we may want to preempt hero with low-priority mission - vt.estimatedReward = new fl::InputVariable("estimatedReward"); //indicate AI that content of the file is important or it is probably bad - vt.value = new fl::OutputVariable("Value"); - vt.value->setMinimum(0); - vt.value->setMaximum(5); - - std::vector helper = {vt.strengthRatio, vt.heroStrength, vt.turnDistance, vt.missionImportance, vt.estimatedReward}; - for(auto val : helper) - { - vt.engine.addInputVariable(val); - } - vt.engine.addOutputVariable(vt.value); - - vt.strengthRatio->addTerm(new fl::Ramp("LOW", SAFE_ATTACK_CONSTANT, 0)); - vt.strengthRatio->addTerm(new fl::Ramp("HIGH", SAFE_ATTACK_CONSTANT, SAFE_ATTACK_CONSTANT * 3)); - vt.strengthRatio->setRange(0, SAFE_ATTACK_CONSTANT * 3); - - //strength compared to our main hero - vt.heroStrength->addTerm(new fl::Ramp("LOW", 0.2, 0)); - vt.heroStrength->addTerm(new fl::Triangle("MEDIUM", 0.2, 0.8)); - vt.heroStrength->addTerm(new fl::Ramp("HIGH", 0.5, 1)); - vt.heroStrength->setRange(0.0, 1.0); - - vt.turnDistance->addTerm(new fl::Ramp("SMALL", 0.5, 0)); - vt.turnDistance->addTerm(new fl::Triangle("MEDIUM", 0.1, 0.8)); - vt.turnDistance->addTerm(new fl::Ramp("LONG", 0.5, 3)); - vt.turnDistance->setRange(0.0, 3.0); - - vt.missionImportance->addTerm(new fl::Ramp("LOW", 2.5, 0)); - vt.missionImportance->addTerm(new fl::Triangle("MEDIUM", 2, 3)); - vt.missionImportance->addTerm(new fl::Ramp("HIGH", 2.5, 5)); - vt.missionImportance->setRange(0.0, 5.0); - - vt.estimatedReward->addTerm(new fl::Ramp("LOW", 2.5, 0)); - vt.estimatedReward->addTerm(new fl::Ramp("HIGH", 2.5, 5)); - vt.estimatedReward->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 - vt.value->addTerm(new fl::Ramp("LOW", 2.5, 0)); - vt.value->addTerm(new fl::Triangle("MEDIUM", 2, 3)); //can't be center of mass :/ - vt.value->addTerm(new fl::Ramp("HIGH", 2.5, 5)); - vt.value->setRange(0.0, 5.0); - - //use unarmed scouts if possible - vt.addRule("if strengthRatio is HIGH and heroStrength is LOW then Value is very HIGH"); - //we may want to use secondary hero(es) rather than main hero - vt.addRule("if strengthRatio is HIGH and heroStrength is MEDIUM then Value is somewhat HIGH"); - vt.addRule("if strengthRatio is HIGH and heroStrength is HIGH then Value is somewhat LOW"); - //don't assign targets to heroes who are too weak, but prefer targets of our main hero (in case we need to gather army) - vt.addRule("if strengthRatio is LOW and heroStrength is LOW then Value is very LOW"); - //attempt to arm secondary heroes is not stupid - vt.addRule("if strengthRatio is LOW and heroStrength is MEDIUM then Value is somewhat HIGH"); - vt.addRule("if strengthRatio is LOW and heroStrength is HIGH then Value is LOW"); - - //do not cancel important goals - vt.addRule("if lockedMissionImportance is HIGH then Value is very LOW"); - vt.addRule("if lockedMissionImportance is MEDIUM then Value is somewhat LOW"); - vt.addRule("if lockedMissionImportance is LOW then Value is HIGH"); - //pick nearby objects if it's easy, avoid long walks - vt.addRule("if turnDistance is SMALL then Value is HIGH"); - vt.addRule("if turnDistance is MEDIUM then Value is MEDIUM"); - vt.addRule("if turnDistance is LONG then Value is LOW"); - //some goals are more rewarding by definition f.e. capturing town is more important than collecting resource - experimental - vt.addRule("if estimatedReward is HIGH then Value is very HIGH"); - vt.addRule("if estimatedReward is LOW then Value is somewhat LOW"); - } - catch(fl::Exception & fe) - { - logAi->error("visitTile: %s", fe.getWhat()); - } -} - -void FuzzyHelper::initWanderTarget() -{ - try - { - wanderTarget.turnDistance = new fl::InputVariable("turnDistance"); //distance on map from object - wanderTarget.objectValue = new fl::InputVariable("objectValue"); //value of that object type known by AI - wanderTarget.value = new fl::OutputVariable("value"); - wanderTarget.value->setMinimum(0); - wanderTarget.value->setMaximum(10); - - wanderTarget.engine.addInputVariable(wanderTarget.turnDistance); - wanderTarget.engine.addInputVariable(wanderTarget.objectValue); - wanderTarget.engine.addOutputVariable(wanderTarget.visitGain); - - //for now distance variable same as in as VisitTile - wanderTarget.turnDistance->addTerm(new fl::Ramp("SHORT", 0.5, 0)); - wanderTarget.turnDistance->addTerm(new fl::Triangle("MEDIUM", 0.1, 0.8)); - wanderTarget.turnDistance->addTerm(new fl::Ramp("LONG", 0.5, 3)); - wanderTarget.turnDistance->setRange(0, 3.0); - - //objectValue ranges are based on checking RMG priorities of some objects and trying to guess sane value ranges - wanderTarget.objectValue->addTerm(new fl::Ramp("LOW", 3000, 0)); //I have feeling that concave shape might work well instead of ramp for objectValue FL terms - wanderTarget.objectValue->addTerm(new fl::Triangle("MEDIUM", 2500, 6000)); - wanderTarget.objectValue->addTerm(new fl::Ramp("HIGH", 5000, 20000)); - wanderTarget.objectValue->setRange(0, 20000); //relic artifact value is border value by design, even better things are scaled down. - - wanderTarget.value->addTerm(new fl::Ramp("LOW", 5, 0)); - wanderTarget.value->addTerm(new fl::Triangle("MEDIUM", 4, 6)); - wanderTarget.value->addTerm(new fl::Ramp("HIGH", 5, 10)); - wanderTarget.value->setRange(0, 10); - - wanderTarget.addRule("if turnDistance is LONG and objectValue is HIGH then value is MEDIUM"); - wanderTarget.addRule("if turnDistance is MEDIUM and objectValue is HIGH then value is somewhat HIGH"); - wanderTarget.addRule("if turnDistance is SHORT and objectValue is HIGH then value is HIGH"); - - wanderTarget.addRule("if turnDistance is LONG and objectValue is MEDIUM then value is somewhat LOW"); - wanderTarget.addRule("if turnDistance is MEDIUM and objectValue is MEDIUM then value is MEDIUM"); - wanderTarget.addRule("if turnDistance is SHORT and objectValue is MEDIUM then value is somewhat HIGH"); - - wanderTarget.addRule("if turnDistance is LONG and objectValue is LOW then value is very LOW"); - wanderTarget.addRule("if turnDistance is MEDIUM and objectValue is LOW then value is LOW"); - wanderTarget.addRule("if turnDistance is SHORT and objectValue is LOW then value is MEDIUM"); - } - catch(fl::Exception & fe) - { - logAi->error("FindWanderTarget: %s", fe.getWhat()); - } -} - float FuzzyHelper::evaluate(Goals::VisitTile & g) { //we assume that hero is already set and we want to choose most suitable one for the mission @@ -635,11 +586,51 @@ void FuzzyHelper::setPriority(Goals::TSubgoal & g) //calls evaluate - Visitor pa g->setpriority(g->accept(this)); //this enforces returned value is set } +EvalWanderTargetObject::EvalWanderTargetObject() +{ + try + { + objectValue = new fl::InputVariable("objectValue"); //value of that object type known by AI + + engine.addInputVariable(turnDistance); + engine.addInputVariable(objectValue); + engine.addOutputVariable(value); + + //objectValue ranges are based on checking RMG priorities of some objects and trying to guess sane value ranges + objectValue->addTerm(new fl::Ramp("LOW", 3000, 0)); //I have feeling that concave shape might work well instead of ramp for objectValue FL terms + objectValue->addTerm(new fl::Triangle("MEDIUM", 2500, 6000)); + objectValue->addTerm(new fl::Ramp("HIGH", 5000, 20000)); + objectValue->setRange(0, 20000); //relic artifact value is border value by design, even better things are scaled down. + + addRule("if turnDistance is LONG and objectValue is HIGH then value is MEDIUM"); + addRule("if turnDistance is MEDIUM and objectValue is HIGH then value is somewhat HIGH"); + addRule("if turnDistance is SHORT and objectValue is HIGH then value is HIGH"); + + addRule("if turnDistance is LONG and objectValue is MEDIUM then value is somewhat LOW"); + addRule("if turnDistance is MEDIUM and objectValue is MEDIUM then value is MEDIUM"); + addRule("if turnDistance is SHORT and objectValue is MEDIUM then value is somewhat HIGH"); + + addRule("if turnDistance is LONG and objectValue is LOW then value is very LOW"); + addRule("if turnDistance is MEDIUM and objectValue is LOW then value is LOW"); + addRule("if turnDistance is SHORT and objectValue is LOW then value is MEDIUM"); + } + catch(fl::Exception & fe) + { + logAi->error("FindWanderTarget: %s", fe.getWhat()); + } + configure(); +} + EvalWanderTargetObject::~EvalWanderTargetObject() { delete objectValue; } +EvalVisitTile::EvalVisitTile() +{ + configure(); +} + EvalVisitTile::~EvalVisitTile() { } diff --git a/AI/VCAI/Fuzzy.h b/AI/VCAI/Fuzzy.h index 5a16beae1..4a9cfdd67 100644 --- a/AI/VCAI/Fuzzy.h +++ b/AI/VCAI/Fuzzy.h @@ -30,6 +30,7 @@ public: class TacticalAdvantage : public engineBase { public: + TacticalAdvantage(); fl::InputVariable * ourWalkers, *ourShooters, *ourFlyers, *enemyWalkers, *enemyShooters, *enemyFlyers; fl::InputVariable * ourSpeed, *enemySpeed; fl::InputVariable * bankPresent; @@ -38,29 +39,34 @@ public: ~TacticalAdvantage(); }; -class HeroMovementGoalEngine : public engineBase +class HeroMovementGoalEngineBase : public engineBase //abstract class for hero visiting goals, allows evaluating newly added elementar objectives with appropiately broad context data { public: + HeroMovementGoalEngineBase(); fl::InputVariable * strengthRatio; fl::InputVariable * heroStrength; fl::InputVariable * turnDistance; fl::InputVariable * missionImportance; fl::InputVariable * estimatedReward; fl::OutputVariable * value; - ~HeroMovementGoalEngine(); + ~HeroMovementGoalEngineBase(); + +private: + float calculateTurnDistanceInputValue(const CGHeroInstance * h, int3 tile) const; }; -class EvalVisitTile : public HeroMovementGoalEngine +class EvalVisitTile : public HeroMovementGoalEngineBase { public: + EvalVisitTile(); ~EvalVisitTile(); }; -class EvalWanderTargetObject : public HeroMovementGoalEngine //designed for use with VCAI::wander() +class EvalWanderTargetObject : public HeroMovementGoalEngineBase //designed for use with VCAI::wander() { public: + EvalWanderTargetObject(); fl::InputVariable * objectValue; - fl::OutputVariable * visitGain; ~EvalWanderTargetObject(); }; @@ -74,17 +80,12 @@ class FuzzyHelper EvalWanderTargetObject wanderTarget; -private: - float calculateTurnDistanceInputValue(const CGHeroInstance * h, int3 tile) const; - public: enum RuleBlocks {BANK_DANGER, TACTICAL_ADVANTAGE, VISIT_TILE}; //blocks should be initialized in this order, which may be confusing :/ FuzzyHelper(); void initTacticalAdvantage(); - void initVisitTile(); - void initWanderTarget(); float evaluate(Goals::Explore & g); float evaluate(Goals::RecruitHero & g);