diff --git a/TacticalAI/CMakeLists.txt b/TacticalAI/CMakeLists.txt index 53ef7ec0..106cf547 100644 --- a/TacticalAI/CMakeLists.txt +++ b/TacticalAI/CMakeLists.txt @@ -14,4 +14,6 @@ set(TacticalAISrc "${CMAKE_CURRENT_SOURCE_DIR}/QuestDebug.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/Realtime.cpp" "${CMAKE_CURRENT_SOURCE_DIR}/ZombieDecideAction.cpp" +"${CMAKE_CURRENT_SOURCE_DIR}/UtilityAI.cpp" +"${CMAKE_CURRENT_SOURCE_DIR}/UtilityAI_ResponseCurve.cpp" PARENT_SCOPE) diff --git a/TacticalAI/UtilityAI.cpp b/TacticalAI/UtilityAI.cpp new file mode 100644 index 00000000..c6b70796 --- /dev/null +++ b/TacticalAI/UtilityAI.cpp @@ -0,0 +1,372 @@ +#include "UtilityAI.h" +#include "UtilityAI_ResponseCurve.h" +#include "Overhead Types.h" +#include "Soldier Control.h" +#include "Isometric Utils.h" +#include "soldier profile type.h" +#include "AIInternals.h" +#include "Render Fun.h" + +namespace UtilityAI +{ + + typedef enum DecisionInput + { + MyHealth, + MyStamina, + MyStance, + MyMorale, + MyCurrentWeaponRange, + DistanceFromMe, + HealthOfTarget, + AllyCount, + EnemyCount, + AllyEnemyRatio, + EnemiesNearTarget, + AlliesNearTarget, + HaveGasmaskInInventory, + HaveNVGInInventory, + HasKnife, + HasThrowingKnife, + HasSmokeGrenade, + IamInSmoke, + IamInTearGas, + IamInMustardGas, // Also fire & creature gas + NearSmoke, + NearTearGas, + NearMustardGas, + InShock, + InsideRoom, + NightTime, + InLightAtNight, + TakenLargeHit, + HasTrait_AutoWeapons, + HasTrait_HeavyWeapons, + HasTrait_Sniper, + HasTrait_Ranger, + HasTrait_Gunslinger, + HasTrait_MartialArts, + HasTrait_SquadLeader, + HasTrait_Technician, + HasTrait_Doctor, + HasTrait_Ambidextrous, + HasTrait_Melee, + HasTrait_Throwing, + HasTrait_NightOps, + HasTrait_Stealthy, + HasTrait_Athletics, + HasTrait_Bodybuilding, + HasTrait_Demolitions, + HasTrait_Teaching, + HasTrait_Scouting, + HasTrait_Covert, + HasTrait_RadioOperator, + HasTrait_Snitch, + HasTrait_Survival, + HasTraitOld_Lockpicking, + HasTraitOld_HandToHand, + HasTraitOld_Electronics, + HasTraitOld_NightOps, + HasTraitOld_Throwing, + HasTraitOld_Teaching, + HasTraitOld_HeavyWeapons, + HasTraitOld_AutoWeapons, + HasTraitOld_Stealthy, + HasTraitOld_Ambidextrous, + HasTraitOld_Thief, + HasTraitOld_MartialArts, + HasTraitOld_Knifing, + HasTraitOld_Sniper, + HasTraitOld_Camouflaged, + + }; + + struct Consideration + { + DecisionInput input; + ResponseCurve curve; + SoldierID target; + INT32 targetLocation; + }; + + struct DecisionScoreEvaluator + { + ActionType Action; + float priorityWeight; + std::string description; + std::vector considerations; + }; + + struct DecisionScore + { + float score; + size_t idx; // Index into DM::decisions + + bool operator<(const DecisionScore& a) const + { + return score < a.score; + } + + bool operator>(const DecisionScore& a) const + { + return score > a.score; + } + + bool operator==(const DecisionScore& a) const + { + return score == a.score; + } + + }; + + struct DecisionMaker + { + std::vector decisions; + std::vector scores; + }; + + static float GetTrait(DecisionInput input, SOLDIERTYPE* soldier) + { + switch ( input ) + { + case HasTrait_AutoWeapons: + return static_cast(HAS_SKILL_TRAIT(soldier, AUTO_WEAPONS_NT)); + case HasTrait_HeavyWeapons: + return static_cast(HAS_SKILL_TRAIT(soldier, HEAVY_WEAPONS_NT)); + case HasTrait_Sniper: + return static_cast(HAS_SKILL_TRAIT(soldier, SNIPER_NT)); + case HasTrait_Ranger: + return static_cast(HAS_SKILL_TRAIT(soldier, RANGER_NT)); + case HasTrait_Gunslinger: + return static_cast(HAS_SKILL_TRAIT(soldier, GUNSLINGER_NT)); + case HasTrait_MartialArts: + return static_cast(HAS_SKILL_TRAIT(soldier, MARTIAL_ARTS_NT)); + case HasTrait_SquadLeader: + return static_cast(HAS_SKILL_TRAIT(soldier, SQUADLEADER_NT)); + case HasTrait_Technician: + return static_cast(HAS_SKILL_TRAIT(soldier, TECHNICIAN_NT)); + case HasTrait_Doctor: + return static_cast(HAS_SKILL_TRAIT(soldier, DOCTOR_NT)); + case HasTrait_Ambidextrous: + return static_cast(HAS_SKILL_TRAIT(soldier, AMBIDEXTROUS_NT)); + case HasTrait_Melee: + return static_cast(HAS_SKILL_TRAIT(soldier, MELEE_NT)); + case HasTrait_Throwing: + return static_cast(HAS_SKILL_TRAIT(soldier, THROWING_NT)); + case HasTrait_NightOps: + return static_cast(HAS_SKILL_TRAIT(soldier, NIGHT_OPS_NT)); + case HasTrait_Stealthy: + return static_cast(HAS_SKILL_TRAIT(soldier, STEALTHY_NT)); + case HasTrait_Athletics: + return static_cast(HAS_SKILL_TRAIT(soldier, ATHLETICS_NT)); + case HasTrait_Bodybuilding: + return static_cast(HAS_SKILL_TRAIT(soldier, BODYBUILDING_NT)); + case HasTrait_Demolitions: + return static_cast(HAS_SKILL_TRAIT(soldier, DEMOLITIONS_NT)); + case HasTrait_Teaching: + return static_cast(HAS_SKILL_TRAIT(soldier, TEACHING_NT)); + case HasTrait_Scouting: + return static_cast(HAS_SKILL_TRAIT(soldier, SCOUTING_NT)); + case HasTrait_Covert: + return static_cast(HAS_SKILL_TRAIT(soldier, COVERT_NT)); + case HasTrait_RadioOperator: + return static_cast(HAS_SKILL_TRAIT(soldier, RADIO_OPERATOR_NT)); + case HasTrait_Snitch: + return static_cast(HAS_SKILL_TRAIT(soldier, SNITCH_NT)); + case HasTrait_Survival: + return static_cast(HAS_SKILL_TRAIT(soldier, SURVIVAL_NT)); + case HasTraitOld_Lockpicking: + return static_cast(HAS_SKILL_TRAIT(soldier, LOCKPICKING_OT)); + case HasTraitOld_HandToHand: + return static_cast(HAS_SKILL_TRAIT(soldier, HANDTOHAND_OT)); + case HasTraitOld_Electronics: + return static_cast(HAS_SKILL_TRAIT(soldier, ELECTRONICS_OT)); + case HasTraitOld_NightOps: + return static_cast(HAS_SKILL_TRAIT(soldier, NIGHTOPS_OT)); + case HasTraitOld_Throwing: + return static_cast(HAS_SKILL_TRAIT(soldier, THROWING_OT)); + case HasTraitOld_Teaching: + return static_cast(HAS_SKILL_TRAIT(soldier, TEACHING_OT)); + case HasTraitOld_HeavyWeapons: + return static_cast(HAS_SKILL_TRAIT(soldier, HEAVY_WEAPS_OT)); + case HasTraitOld_AutoWeapons: + return static_cast(HAS_SKILL_TRAIT(soldier, AUTO_WEAPS_OT)); + case HasTraitOld_Stealthy: + return static_cast(HAS_SKILL_TRAIT(soldier, STEALTHY_OT)); + case HasTraitOld_Ambidextrous: + return static_cast(HAS_SKILL_TRAIT(soldier, AMBIDEXT_OT)); + case HasTraitOld_Thief: + return static_cast(HAS_SKILL_TRAIT(soldier, THIEF_OT)); + case HasTraitOld_MartialArts: + return static_cast(HAS_SKILL_TRAIT(soldier, MARTIALARTS_OT)); + case HasTraitOld_Knifing: + return static_cast(HAS_SKILL_TRAIT(soldier, KNIFING_OT)); + case HasTraitOld_Sniper: + return static_cast(HAS_SKILL_TRAIT(soldier, PROF_SNIPER_OT)); + case HasTraitOld_Camouflaged: + return static_cast(HAS_SKILL_TRAIT(soldier, CAMOUFLAGED_OT)); + } + + return 0.0f; + } + + float GetInputValue(DecisionInput input, SOLDIERTYPE* me, SoldierID target, INT32 targetLocation) + { + switch ( input ) + { + case MyHealth: + { + const auto health = static_cast(me->stats.bLife); + const auto min = 0.0f; + const auto max = static_cast(me->stats.bLifeMax); + return NormalizeInput(health, min, max); + } + case MyMorale: + { + const auto morale = static_cast(me->aiData.bAIMorale); + const auto min = 0.0f; + const auto max = static_cast(MORALE_FEARLESS); + return NormalizeInput(morale, min, max); + } + case HaveGasmaskInInventory: + { + const auto gasmaskInvSlot = FindGasMask(me); + if ( gasmaskInvSlot != NO_SLOT ) { return 1.0f; } + else { return 0.0f; } + } + case HasKnife: + { + const auto bWeaponIn = FindAIUsableObjClass(me, (IC_BLADE)); + if ( bWeaponIn != NO_SLOT ) { return 1.0f; } + else { return 0.0f; } + } + case HasThrowingKnife: + { + const auto bWeaponIn = FindAIUsableObjClass(me, (IC_THROWING_KNIFE)); + if ( bWeaponIn != NO_SLOT ) { return 1.0f; } + else { return 0.0f; } + } + case HasSmokeGrenade: + { + const auto bGrenadeIn = FindThrowableGrenade(me, EXPLOSV_SMOKE); + if ( bGrenadeIn != NO_SLOT ) { return 1.0f; } + else { return 0.0f; } + } + case DistanceFromMe: + { + const auto distance = PythSpacesAway(me->sGridNo, targetLocation); + } + case IamInSmoke: + return static_cast(InSmoke(me, me->sGridNo)); + case IamInTearGas: + return static_cast(InTearGas(me, me->sGridNo)); + case IamInMustardGas: + return static_cast(InMustardGas(me, me->sGridNo)); + case NearSmoke: + + case NearTearGas: + + case NearMustardGas: + + case InShock: + + case InsideRoom: + return static_cast(InARoom(me->sGridNo, NULL)); + case NightTime: + + case InLightAtNight: + + case TakenLargeHit: + return static_cast(me->TakenLargeHit()); + + case HasTrait_AutoWeapons: + case HasTrait_HeavyWeapons: + case HasTrait_Sniper: + case HasTrait_Ranger: + case HasTrait_Gunslinger: + case HasTrait_MartialArts: + case HasTrait_SquadLeader: + case HasTrait_Technician: + case HasTrait_Doctor: + case HasTrait_Ambidextrous: + case HasTrait_Melee: + case HasTrait_Throwing: + case HasTrait_NightOps: + case HasTrait_Stealthy: + case HasTrait_Athletics: + case HasTrait_Bodybuilding: + case HasTrait_Demolitions: + case HasTrait_Teaching: + case HasTrait_Scouting: + case HasTrait_Covert: + case HasTrait_RadioOperator: + case HasTrait_Snitch: + case HasTrait_Survival: + case HasTraitOld_Lockpicking: + case HasTraitOld_HandToHand: + case HasTraitOld_Electronics: + case HasTraitOld_NightOps: + case HasTraitOld_Throwing: + case HasTraitOld_Teaching: + case HasTraitOld_HeavyWeapons: + case HasTraitOld_AutoWeapons: + case HasTraitOld_Stealthy: + case HasTraitOld_Ambidextrous: + case HasTraitOld_Thief: + case HasTraitOld_MartialArts: + case HasTraitOld_Knifing: + case HasTraitOld_Sniper: + case HasTraitOld_Camouflaged: + return GetTrait(input, me); + } + } + + float ScoreDSE(DecisionScoreEvaluator DSE, SOLDIERTYPE* me) + { + float finalScore = 1.0f; + + for ( const auto& consideration : DSE.considerations ) + { + if ( finalScore <= 0.0f ) { break; } + + float curveInput = GetInputValue(consideration.input, me, consideration.target, consideration.targetLocation); + float considerationScore = CalculateResponse(curveInput, consideration.curve); + + finalScore *= considerationScore; + } + + // Add consideration compensation + const auto modificationFactor = 1 - 1 / (DSE.considerations.size()); + const auto makeUpValue = (1 - finalScore) * modificationFactor; + finalScore += finalScore * makeUpValue; + + // Add DSE priority weight + finalScore *= DSE.priorityWeight; + + return finalScore; + } + + void ScoreAllDecisions(DecisionMaker DM, SOLDIERTYPE* me) + { + size_t idx = 0; + for ( const auto& decision : DM.decisions ) + { + DM.scores[idx].score = ScoreDSE(decision, me); + DM.scores[idx].idx = idx; + ++idx; + } + + // Sort all scores from best to last + std::sort(DM.scores.begin(), DM.scores.end(), std::greater()); + } + + ActionType SelectScoredAction(DecisionMaker DM) + { + // Select highest for now. TODO weighted random choice + const auto idx = DM.scores[0].idx; + const auto action = DM.decisions[idx].Action; + + return action; + } + +} diff --git a/TacticalAI/UtilityAI.h b/TacticalAI/UtilityAI.h new file mode 100644 index 00000000..56ac7cca --- /dev/null +++ b/TacticalAI/UtilityAI.h @@ -0,0 +1,5 @@ +#pragma once + +//void ScoreAllDecisions(DecisionMaker DM, SOLDIERTYPE* me); +//ActionType SelectScoredAction(DecisionMaker DM); + diff --git a/TacticalAI/UtilityAI_ResponseCurve.cpp b/TacticalAI/UtilityAI_ResponseCurve.cpp new file mode 100644 index 00000000..d0f5aa84 --- /dev/null +++ b/TacticalAI/UtilityAI_ResponseCurve.cpp @@ -0,0 +1,57 @@ +#include "UtilityAI_ResponseCurve.h" + +#include +#include + +namespace UtilityAI +{ + + float NormalizeInput(float value, float min, float max) + { + const float normalizedValue = (value - min) / (max - min); + return normalizedValue; + } + + float CalculateResponse(float x, ResponseCurve curve) + { + const auto min = curve.min; + const auto max = curve.max; + const auto type = curve.type; + const auto m = curve.m; + const auto k = curve.k; + const auto b = curve.b; + const auto c = curve.c; + + float input = std::clamp(x, min, max); + input = NormalizeInput(input, min, max); + + float y = 0.0f; + // Polynomial + float n; + // Logistic + const float e = 2.71828f; // Euler's number. I think? Lecture slides did not explain if this is what it's supposed to be. + const float exponent = -input + c; + const float denominator = 1.0f + 1000.0f * e * pow(m, exponent); + + switch ( type ) + { + case Step: + if ( input != 0.0f ) { return 1.0f; } + else { return 0.0f; } + case Linear: + case Polynomial: + n = input - c; + y = m * pow(n, k) + b; + break; + case Logistic: + y = (k / denominator) + b; + break; + //case Logit: // Not implemented yet + //break; + } + + y = std::clamp(y, 0.0f, 1.0f); + return y; + } + +} diff --git a/TacticalAI/UtilityAI_ResponseCurve.h b/TacticalAI/UtilityAI_ResponseCurve.h new file mode 100644 index 00000000..e5002842 --- /dev/null +++ b/TacticalAI/UtilityAI_ResponseCurve.h @@ -0,0 +1,24 @@ +#pragma once + +typedef enum +{ + Step, + Linear, + Polynomial, + Logistic, + Logit +} ResponseCurveType; + +struct ResponseCurve +{ + ResponseCurveType type; + float m; + float k; + float b; + float c; + float min; + float max; +}; + +float NormalizeInput(float value, float min, float max); +float CalculateResponse(float x, ResponseCurve curve);