Add skeleton for utilityAI framework

This commit is contained in:
Asdow
2026-01-19 22:59:30 +02:00
parent 58ea3be2fd
commit ff96d653e2
5 changed files with 460 additions and 0 deletions
+2
View File
@@ -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)
+372
View File
@@ -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<Consideration> 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<DecisionScoreEvaluator> decisions;
std::vector<DecisionScore> scores;
};
static float GetTrait(DecisionInput input, SOLDIERTYPE* soldier)
{
switch ( input )
{
case HasTrait_AutoWeapons:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, AUTO_WEAPONS_NT));
case HasTrait_HeavyWeapons:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, HEAVY_WEAPONS_NT));
case HasTrait_Sniper:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, SNIPER_NT));
case HasTrait_Ranger:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, RANGER_NT));
case HasTrait_Gunslinger:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, GUNSLINGER_NT));
case HasTrait_MartialArts:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, MARTIAL_ARTS_NT));
case HasTrait_SquadLeader:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, SQUADLEADER_NT));
case HasTrait_Technician:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, TECHNICIAN_NT));
case HasTrait_Doctor:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, DOCTOR_NT));
case HasTrait_Ambidextrous:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, AMBIDEXTROUS_NT));
case HasTrait_Melee:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, MELEE_NT));
case HasTrait_Throwing:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, THROWING_NT));
case HasTrait_NightOps:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, NIGHT_OPS_NT));
case HasTrait_Stealthy:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, STEALTHY_NT));
case HasTrait_Athletics:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, ATHLETICS_NT));
case HasTrait_Bodybuilding:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, BODYBUILDING_NT));
case HasTrait_Demolitions:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, DEMOLITIONS_NT));
case HasTrait_Teaching:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, TEACHING_NT));
case HasTrait_Scouting:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, SCOUTING_NT));
case HasTrait_Covert:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, COVERT_NT));
case HasTrait_RadioOperator:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, RADIO_OPERATOR_NT));
case HasTrait_Snitch:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, SNITCH_NT));
case HasTrait_Survival:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, SURVIVAL_NT));
case HasTraitOld_Lockpicking:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, LOCKPICKING_OT));
case HasTraitOld_HandToHand:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, HANDTOHAND_OT));
case HasTraitOld_Electronics:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, ELECTRONICS_OT));
case HasTraitOld_NightOps:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, NIGHTOPS_OT));
case HasTraitOld_Throwing:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, THROWING_OT));
case HasTraitOld_Teaching:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, TEACHING_OT));
case HasTraitOld_HeavyWeapons:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, HEAVY_WEAPS_OT));
case HasTraitOld_AutoWeapons:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, AUTO_WEAPS_OT));
case HasTraitOld_Stealthy:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, STEALTHY_OT));
case HasTraitOld_Ambidextrous:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, AMBIDEXT_OT));
case HasTraitOld_Thief:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, THIEF_OT));
case HasTraitOld_MartialArts:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, MARTIALARTS_OT));
case HasTraitOld_Knifing:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, KNIFING_OT));
case HasTraitOld_Sniper:
return static_cast<float>(HAS_SKILL_TRAIT(soldier, PROF_SNIPER_OT));
case HasTraitOld_Camouflaged:
return static_cast<float>(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<float>(me->stats.bLife);
const auto min = 0.0f;
const auto max = static_cast<float>(me->stats.bLifeMax);
return NormalizeInput(health, min, max);
}
case MyMorale:
{
const auto morale = static_cast<float>(me->aiData.bAIMorale);
const auto min = 0.0f;
const auto max = static_cast<float>(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<float>(InSmoke(me, me->sGridNo));
case IamInTearGas:
return static_cast<float>(InTearGas(me, me->sGridNo));
case IamInMustardGas:
return static_cast<float>(InMustardGas(me, me->sGridNo));
case NearSmoke:
case NearTearGas:
case NearMustardGas:
case InShock:
case InsideRoom:
return static_cast<float>(InARoom(me->sGridNo, NULL));
case NightTime:
case InLightAtNight:
case TakenLargeHit:
return static_cast<float>(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<DecisionScore>());
}
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;
}
}
+5
View File
@@ -0,0 +1,5 @@
#pragma once
//void ScoreAllDecisions(DecisionMaker DM, SOLDIERTYPE* me);
//ActionType SelectScoredAction(DecisionMaker DM);
+57
View File
@@ -0,0 +1,57 @@
#include "UtilityAI_ResponseCurve.h"
#include <cmath>
#include <algorithm>
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;
}
}
+24
View File
@@ -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);