163 lines
8.9 KiB
Python
163 lines
8.9 KiB
Python
"""Execute actual delivered-belief pursuit rules as C++; no engine/AI behavior claim."""
|
|
import re
|
|
from policy_harness import ROOT, body, run
|
|
P = ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQPursuit.c'
|
|
source = P.read_text()
|
|
methods = '\n'.join(body(P, signature) for signature in (
|
|
'static bool Finite(', 'static bool PositionValid(', 'static int Limit(', 'static bool Actionable(',
|
|
'static bool Heading(', 'static ERR_HQPursuitRole Role(', 'static float LifetimeSeconds(',
|
|
'static float TaskLifetimeSeconds(', 'static bool CanCommit(', 'static int Bearing(', 'static bool BearingAllowed(', 'static vector Direction(',
|
|
'static vector Objective(', 'static float Score(', 'static bool Better('))
|
|
methods = methods.replace('Math.', 'Math::').replace('vector.', 'vector::')
|
|
methods = methods.replace('ERR_HQPursuitRole.', 'ERR_HQPursuitRole::')
|
|
methods = methods.replace('vector offset;', 'vector offset{};')
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <cstdio>
|
|
#include <limits>
|
|
#include <vector>
|
|
#include <set>
|
|
enum class ERR_HQPursuitRole { SEARCH, INTERCEPT, CONTAIN };
|
|
struct Math {
|
|
static float Clamp(float n,float lo,float hi){return std::clamp(n,lo,hi);}
|
|
static float AbsFloat(float n){return std::abs(n);}
|
|
static float Min(float a,float b){return std::min(a,b);}
|
|
};
|
|
struct vector {
|
|
float x=0,y=0,z=0;
|
|
vector()=default;
|
|
vector(float a,float b,float c):x(a),y(b),z(c){}
|
|
vector(const char* text){sscanf(text,"%f %f %f",&x,&y,&z);}
|
|
float& operator[](int i){return i==0?x:i==1?y:z;}
|
|
float operator[](int i)const{return i==0?x:i==1?y:z;}
|
|
vector operator+(vector b)const{return {x+b.x,y+b.y,z+b.z};}
|
|
vector operator-(vector b)const{return {x-b.x,y-b.y,z-b.z};}
|
|
vector operator*(float v)const{return {x*v,y*v,z*v};}
|
|
float Length()const{return std::sqrt(x*x+y*y+z*z);}
|
|
vector Normalized()const{float n=Length();return {x/n,y/n,z/n};}
|
|
static float Dot(vector a,vector b){return a.x*b.x+a.y*b.y+a.z*b.z;}
|
|
static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}
|
|
};
|
|
vector Vector(float a,float b,float c){return {a,b,c};}
|
|
struct Rules {
|
|
'''+methods+r'''
|
|
};
|
|
bool near(float a,float b){return std::abs(a-b)<0.001f;}
|
|
int main(){
|
|
using Role=ERR_HQPursuitRole;
|
|
assert(Rules::Limit(-1)==0&&Rules::Limit(0)==0&&Rules::Limit(1)==1);
|
|
assert(Rules::Limit(3)==3&&Rules::Limit(8)==3);
|
|
assert(Rules::Actionable(.9,10,50)&&Rules::Actionable(.15,600,2000));
|
|
assert(!Rules::Actionable(.149,10,50)&&Rules::Actionable(.9,601,50));
|
|
assert(Rules::Actionable(.2,1200,1900)&&!Rules::Actionable(.2,1200,2001));
|
|
assert(!Rules::Actionable(.9,-1,50)&&!Rules::Actionable(.9,0,-1));
|
|
float nan=std::numeric_limits<float>::quiet_NaN();
|
|
float inf=std::numeric_limits<float>::infinity();
|
|
assert(!Rules::Actionable(nan,0,50)&&!Rules::Actionable(.9,inf,50));
|
|
assert(!Rules::Actionable(.9,0,nan)&&!Rules::Actionable(1.1,0,50));
|
|
vector north{0,0,5}, center{1000,12,1000};
|
|
assert(Rules::Heading(north,true)&&!Rules::Heading(north,false));
|
|
assert(!Rules::Heading({0,0,0},true)&&!Rules::Heading({0,0,71},true));
|
|
assert(!Rules::Heading({nan,0,5},true));
|
|
assert(Rules::Role(.9,10,50,true,0)==Role::INTERCEPT);
|
|
assert(Rules::Role(.9,10,50,true,1)==Role::SEARCH);
|
|
assert(Rules::Role(.9,10,50,true,2)==Role::CONTAIN);
|
|
for(int slot=0;slot<3;slot++){
|
|
assert(Rules::Role(.64,10,50,true,slot)==Role::SEARCH);
|
|
assert(Rules::Role(.9,46,50,true,slot)==Role::SEARCH);
|
|
assert(Rules::Role(.9,10,151,true,slot)==Role::SEARCH);
|
|
assert(Rules::Role(.9,10,50,false,slot)==Role::SEARCH);
|
|
}
|
|
assert(Rules::Role(.79,10,50,true,2)==Role::SEARCH);
|
|
assert(Rules::Role(.9,10,101,true,2)==Role::SEARCH);
|
|
assert(Rules::Role(.9,10,50,true,-1)==Role::SEARCH);
|
|
assert(Rules::LifetimeSeconds(Role::INTERCEPT)==60);
|
|
assert(Rules::LifetimeSeconds(Role::CONTAIN)==90);
|
|
assert(Rules::LifetimeSeconds(Role::SEARCH)==120);
|
|
assert(Rules::TaskLifetimeSeconds(Role::SEARCH,0)==120);
|
|
assert(near(Rules::TaskLifetimeSeconds(Role::SEARCH,2000),2000.f/3+45));
|
|
assert(Rules::TaskLifetimeSeconds(Role::SEARCH,5000)==900);
|
|
assert(Rules::TaskLifetimeSeconds(Role::INTERCEPT,2000)==60);
|
|
assert(Rules::TaskLifetimeSeconds(Role::CONTAIN,2000)==90);
|
|
assert(Rules::TaskLifetimeSeconds(Role::SEARCH,-1)==0);
|
|
assert(Rules::TaskLifetimeSeconds(Role::SEARCH,nan)==0);
|
|
assert(Rules::TaskLifetimeSeconds(Role::SEARCH,inf)==0);
|
|
// The caller must already own a physical, eligible and unassigned formation.
|
|
assert(Rules::CanCommit(true,true,false,false,0,0,3));
|
|
assert(!Rules::CanCommit(false,true,false,false,0,0,3));
|
|
assert(!Rules::CanCommit(true,false,false,false,0,0,3));
|
|
assert(!Rules::CanCommit(true,true,true,false,0,0,3));
|
|
assert(!Rules::CanCommit(true,true,false,false,0,-1,3));
|
|
assert(!Rules::CanCommit(true,true,false,false,0,0,0));
|
|
assert(!Rules::CanCommit(true,true,false,false,0,3,99));
|
|
assert(!Rules::CanCommit(true,true,false,true,1,0,3));
|
|
int homeAvailable=3,assigned=0;
|
|
for(int i=0;i<5;i++)if(Rules::CanCommit(true,true,false,true,homeAvailable,assigned,3)){
|
|
homeAvailable--;assigned++;
|
|
}
|
|
assert(homeAvailable==1&&assigned==2);
|
|
// Pending orders spend the slot immediately, and each busy unit is ineligible.
|
|
bool busy=false;int commitments=0;
|
|
for(int i=0;i<5;i++)if(Rules::CanCommit(true,true,busy,false,1,commitments,3)){
|
|
busy=true;commitments++;
|
|
}
|
|
assert(commitments==1);
|
|
vector intercept=Rules::Objective(center,north,true,.9,10,50,Role::INTERCEPT,0);
|
|
assert(near(intercept.x,1000)&&near(intercept.z,1065)&&near(intercept.y,12));
|
|
vector contain=Rules::Objective(center,north,true,.9,10,50,Role::CONTAIN,2);
|
|
assert(near(contain.x,1080)&&near(contain.z,1080));
|
|
vector stale=Rules::Objective(center,north,true,.4,180,400,Role::INTERCEPT,0);
|
|
assert(near(stale.z,1250)); // degraded to a bounded search sector, no velocity lead
|
|
for(int slot=-8;slot<9;slot++)for(float radius:{0.f,50.f,200.f,2000.f}){
|
|
vector objective=Rules::Objective(center,north,true,.2,300,radius,Role::SEARCH,slot);
|
|
assert(vector::DistanceXZ(center,objective)<=300&&near(objective.y,center.y));
|
|
}
|
|
vector maximum=Rules::Objective(center,{0,0,70},true,.9,0,50,Role::INTERCEPT,0);
|
|
assert(near(maximum.z,1200)); // future lead capped even for a fast vehicle
|
|
vector noDoubleAge=Rules::Objective(center,north,true,.9,40,50,Role::INTERCEPT,0);
|
|
assert(near(noDoubleAge.z,1060)); // supplied position is already memory-predicted
|
|
vector first=Rules::Objective(center,north,false,.4,180,100,Role::SEARCH,0);
|
|
vector second=Rules::Objective(center,north,false,.4,180,100,Role::SEARCH,1);
|
|
vector third=Rules::Objective(center,north,false,.4,180,100,Role::SEARCH,2);
|
|
assert(vector::DistanceXZ(first,second)>100&&vector::DistanceXZ(second,third)>100);
|
|
// Nearby units already ahead win interception; nearby correct-flank units contain.
|
|
float ahead=Rules::Score(Role::INTERCEPT,center,north,{1000,0,1200},intercept);
|
|
float behind=Rules::Score(Role::INTERCEPT,center,north,{1000,0,800},intercept);
|
|
assert(ahead>=0&&behind<0);
|
|
float right=Rules::Score(Role::CONTAIN,center,north,{1100,0,1050},contain);
|
|
float left=Rules::Score(Role::CONTAIN,center,north,{900,0,1050},contain);
|
|
assert(right<left);
|
|
assert(Rules::Score(Role::INTERCEPT,center,north,{intercept.x+150,0,intercept.z},intercept)>=0);
|
|
assert(Rules::Score(Role::INTERCEPT,center,north,{intercept.x+151,0,intercept.z},intercept)<0);
|
|
assert(Rules::Score(Role::CONTAIN,center,north,{contain.x+240,0,contain.z},contain)>=0);
|
|
assert(Rules::Score(Role::CONTAIN,center,north,{contain.x+241,0,contain.z},contain)<0);
|
|
assert(Rules::Score(Role::SEARCH,center,north,{1000,0,1000},first)
|
|
<Rules::Score(Role::SEARCH,center,north,{2000,0,1000},first));
|
|
assert(Rules::Score(Role::SEARCH,{nan,0,0},north,center,first)<0);
|
|
assert(Rules::Better(100,2,100,5)&&!Rules::Better(100,5,100,2));
|
|
assert(Rules::Better(99,5,100,2)&&Rules::Better(100,1,-1,-1));
|
|
assert(!Rules::Better(-1,1,100,2)&&!Rules::Better(nan,1,100,2));
|
|
int firstBearing=-1,secondBearing=-1;std::set<int> bearings;
|
|
std::vector<vector> forces={{1000,0,1200},{1200,0,1000},{1000,0,800},{800,0,1000}};
|
|
for(vector force:forces){
|
|
int bearing=Rules::Bearing(force,center);
|
|
if(!Rules::BearingAllowed(bearing,firstBearing,secondBearing))continue;
|
|
bearings.insert(bearing);
|
|
if(firstBearing<0)firstBearing=bearing;
|
|
else if(bearing!=firstBearing&&secondBearing<0)secondBearing=bearing;
|
|
}
|
|
assert(bearings.size()==2);
|
|
assert(Rules::BearingAllowed(firstBearing,firstBearing,secondBearing));
|
|
assert(!Rules::BearingAllowed(2,0,1)&&!Rules::BearingAllowed(3,0,1));
|
|
assert(!Rules::BearingAllowed(-1,-1,-1)&&!Rules::BearingAllowed(2,-1,1));
|
|
assert(Rules::Bearing({nan,0,0},center)==-1);
|
|
}
|
|
''')
|
|
for forbidden in ('GetGame(', 'GetOrigin(', 'GetPlayers(', 'GetControlledEntity(',
|
|
'Spawn(', 'CallLater(', 'SCR_AIGroup', 'IEntity'):
|
|
assert forbidden not in source, f'pursuit must consume snapshots only: {forbidden}'
|
|
assert all(len(line)<120 for line in source.splitlines())
|
|
print('PASS: actual bounded pursuit roles/objectives/scores, stale search degradation, physical/busy/home guards, finite lifetime and two-bearing escape gap; no hidden player reads or spawning')
|