Files
EscapistsReintroduced/tests/test_hq_hypotheses.py
T
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00

123 lines
8.0 KiB
Python

"""Execute actual bounded hypothesis/road geometry code; native road cache remains an engine boundary."""
from policy_harness import ROOT, body, run
import re
P=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQHypotheses.c'
def cpp(s):
s=s.replace('protected ', '').replace('Math.','Math::').replace('vector.','vector::')
for c in ('ERR_HQHypothesisRules','ERR_HQHypothesisKind','ERR_HQMemoryKind','ERR_HQCoverageRules','ERR_HQPursuitRules'):
s=s.replace(c+'.',c+'::')
s=s.replace('out array<ref ERR_HQHypothesis> hypotheses','Array<ERR_HQHypothesis*>& hypotheses')
s=s.replace('array<vector> points','Array<vector>* points').replace('out float ','float& ').replace('out vector ','vector& ')
for c in ('ERR_HQBelief','ERR_HQLocalCoverage','ERR_HQHypothesisRoadCache','ERR_HQHypothesisRoad','ERR_HQHypothesis'):
s=re.sub(r'\b'+c+r' (\w+)',c+r'* \1',s)
s=s.replace('nearest, alternative','nearest=nullptr, *alternative=nullptr')
for name in ('points','belief','result','road','cache','coverage','hypothesis'):
s=s.replace(name+'.',name+'->')
s=s.replace('points[','(*points)[')
s=s.replace('hypotheses[1].','hypotheses[1]->')
s=s.replace('foreach (ERR_HQHypothesisRoad* road : cache->Roads)','for (ERR_HQHypothesisRoad* road : cache->Roads)')
s=s.replace('foreach (ERR_HQHypothesis* hypothesis : hypotheses)','for (ERR_HQHypothesis* hypothesis : hypotheses)')
# Enforce locals are zero-initialized.
s=re.sub(r'\bfloat (traversed|length|total);',r'float \1=0;',s).replace('bool found;','bool found=false;')
return s
rules='\n'.join(cpp(body(P,s)) for s in ('static float MinSpeed(', 'static float MaxSpeed(',
'static float PlanningSpeed(', 'static float Lead(', 'static float Earliest(', 'static float Latest(',
'static float BaseWeight(', 'static bool Project(', 'static float Length(', 'static bool Sample('))
methods='\n'.join(cpp(body(P,s)) for s in ('protected ERR_HQHypothesis Candidate(', 'protected bool Refine(', 'bool Build('))
run(r'''
#include <cassert>
#include <cmath>
#include <algorithm>
#include <vector>
#include <string>
#include <limits>
using string=std::string;
struct vector{
float x=0,y=0,z=0;float& operator[](int i){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};}
vector operator/(float v)const{return *this*(1/v);}
float Length()const{return std::sqrt(x*x+y*y+z*z);}
vector Normalized()const{return *this/Length();}
static vector Lerp(vector a,vector b,float n){return a+(b-a)*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 Math{static float Clamp(float v,float a,float b){return std::clamp(v,a,b);}static float AbsFloat(float v){return std::abs(v);}};
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}void Insert(T v){this->push_back(v);}void Clear(){this->clear();}};
struct ERR_HQPursuitRules{
static bool Finite(float v){return std::isfinite(v);}
static bool PositionValid(vector p){return Finite(p.x)&&Finite(p.y)&&Finite(p.z);}
static bool Heading(vector p,bool h){p.y=0;return h&&PositionValid(p)&&p.Length()>=.5&&p.Length()<=70;}
static vector Direction(vector p,bool h){p.y=0;return Heading(p,h)?p.Normalized():Vector(0,0,1);}
static bool Actionable(float c,float age,float r){return Finite(c)&&Finite(age)&&Finite(r)&&c>=.15&&c<=1&&age>=0&&r>=0&&r<=2000;}
};
struct ERR_HQCoverageRules{static bool PositionValid(vector p){return ERR_HQPursuitRules::PositionValid(p)&&std::abs(p.x)<1000000&&std::abs(p.z)<1000000;}};
struct ERR_HQHypothesisKind{enum{HIDE,FORWARD,REVERSE,LEFT,RIGHT,ALTERNATE_ROAD};};
struct ERR_HQMemoryKind{enum{SIGHTING,GUNFIRE};};
struct ERR_HQBelief{int BeliefId=1,BeliefRevision=2,MessageId=7,Kind=0;vector Position{1000,0,1000},Velocity{0,0,4};float ObservedAt=0,Confidence=1,Radius=80,Speed=4;bool HasVelocity=true;string TravelMode="on foot";};
struct ERR_HQHypothesis{int Id=0,Kind=0,BeliefId=0,BeliefRevision=0,ReportId=0;vector Position;float Weight=0,Radius=0,EarliestSeconds=0,LatestSeconds=0;bool OnRoad=false;};
struct ERR_HQHypothesisRoad{Array<vector> points;Array<vector>*Points=&points;float Length=0;};
struct ERR_HQHypothesisRoadCache{Array<ERR_HQHypothesisRoad*> Roads;};
struct ERR_HQLocalCoverage{int calls=0;float Multiplier(vector p,float,float){calls++;return p.z>1100?.25:1;}};
struct ERR_HQHypothesisRules{
'''+rules+r'''
};
struct Planner{
ERR_HQHypothesisRoadCache roads;int surveys=0;
ERR_HQHypothesisRoadCache*Cache(vector,float){surveys++;return &roads;}
'''+methods+r'''
};
int main(){
using R=ERR_HQHypothesisRules;
float nan=std::numeric_limits<float>::quiet_NaN();
assert(R::Lead(0,4)==180&&R::Lead(30,4)==300&&R::Lead(900,25)==1800);
assert(R::Lead(-1,4)==0&&R::Lead(nan,4)==0);
assert(R::PlanningSpeed("on foot",nan)==1.5&&R::PlanningSpeed("vehicle",50)==25);
Array<vector> bend={{0,0,0},{100,0,0},{100,0,200}};float along,distance;vector p,d;
assert(R::Project(&bend,{50,0,10},along,distance)&&along==50&&distance==10);
assert(R::Sample(&bend,150,p,d)&&p.x==100&&p.z==50); // follows bend, never chord
assert(!R::Project(&bend,{50,10,0},along,distance)); // overpass height mismatch
assert(!R::Project(&bend,{50,0,-76},along,distance));
assert(!R::Sample(&bend,301,p,d)&&!R::Sample(&bend,-1,p,d));
Array<vector> malformed={{0,0,0},{nan,0,1}};assert(!R::Project(&malformed,{0,0,0},along,distance));
Planner planner;ERR_HQBelief b;Array<ERR_HQHypothesis*> out;
assert(planner.Build(&b,nullptr,30000,out)&&out.Count()==5);
float total=0;for(auto*h:out){total+=h->Weight;assert(h->BeliefId==1&&h->BeliefRevision==2&&h->ReportId==7);assert(h->LatestSeconds>=h->EarliestSeconds);}
assert(std::abs(total-1)<.0001&&out[0]->Position.z==1000&&out[1]->Position.z==1300);
assert(out[1]->EarliestSeconds==50&&out[1]->LatestSeconds==460); // since observation
assert(out[1]->Weight>out[2]->Weight&&out[2]->Position.z<1000);
auto originalForward=out[1]->Weight;ERR_HQLocalCoverage coverage;
assert(planner.Build(&b,&coverage,30000,out)&&coverage.calls==5&&out[1]->Weight<originalForward);
b.HasVelocity=false;assert(planner.Build(&b,nullptr,30000,out));assert(out[1]->Weight==out[2]->Weight);
b.HasVelocity=true;
ERR_HQHypothesisRoad road;road.points={{1000,0,800},{1000,0,1100},{1200,0,1100},{1200,0,1700}};road.Length=1100;
planner.roads.Roads.Insert(&road);
assert(planner.Build(&b,nullptr,30000,out)&&out[1]->OnRoad&&out[2]->OnRoad);
assert(out[1]->Position.x==1200&&out[1]->Position.z==1100); // actual road bend
assert(out[1]->EarliestSeconds==50);
ERR_HQHypothesisRoad cross;cross.points={{960,0,900},{960,0,1700}};cross.Length=800;planner.roads.Roads.Insert(&cross);
assert(planner.Build(&b,nullptr,30000,out)&&out.Count()==6&&out[5]->OnRoad);
assert(out[5]->Position.x==960); // independent alternate, no splice at crossing
int surveys=planner.surveys;b.Kind=ERR_HQMemoryKind::GUNFIRE;
assert(planner.Build(&b,nullptr,30000,out)&&out.Count()==5&&planner.surveys==surveys);
for(auto*h:out)assert(!h->OnRoad);
b.Kind=0;b.ObservedAt=30001;assert(!planner.Build(&b,nullptr,30000,out)&&out.empty());
b.ObservedAt=0;b.TravelMode="aircraft";assert(!planner.Build(&b,nullptr,30000,out));
b.TravelMode="on foot";b.Position.x=nan;assert(!planner.Build(&b,nullptr,30000,out));
b.Position.x=1000;b.Confidence=.14;assert(!planner.Build(&b,nullptr,30000,out));
assert(!planner.Build(nullptr,nullptr,30000,out));
}
''')
source=P.read_text()
for forbidden in ('GetPlayers(', 'GetPlayerManager(', 'GetControlledEntity(', '.Target', 'GetOrigin(', 'Spawn('):
assert forbidden not in source
step=body(P,'void Step(')
assert 'Pending.Count() > 128' in step and '> 4096' in step and 'examined < 4' in step
assert 'm_Caches.Count() >= 8' in source and 'Points.Insert(point)' in source
print('PASS: actual five/six bounded hypotheses, observation-relative ETA, future lead, hiding/reverse, local negative weighting, immutable report lineage, road bends/heights and finite cache budgets')