Merge owned systems into one addon while preserving resource identities. Gate player admission on the replicated prison scene, restore the original framed door, and hold enemy AI until escape. Correct scenario branding and author metadata, include publishing assets, and retain regression and native verification evidence.
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/core/Scripts/Game/ERR/10_AI/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')
|