182 lines
13 KiB
Python
182 lines
13 KiB
Python
"""Run the actual bounded memory implementation with value-only native adapters.
|
|
|
|
No fake world polling: identity stubs deliberately have no position/health API.
|
|
"""
|
|
import re
|
|
from policy_harness import ROOT, run
|
|
path = ROOT / 'addons/core/Scripts/Game/ERR/10_AI/ERR_HQMemory.c'
|
|
source = path.read_text()
|
|
for forbidden in ('GetOrigin(', 'GetControlledEntity(', 'GetPlayerManager(', 'GetDamageManager(', 'GetWorld('):
|
|
assert forbidden not in source
|
|
assert max(map(len, source.splitlines())) <= 120
|
|
code = source
|
|
for kind in ('ERR_HQReport', 'ERR_HQMemoryTrack'):
|
|
code = code.replace('ref array<ref ' + kind + '>', 'Array<' + kind + '*>')
|
|
for kind in ('int', 'float'):
|
|
code = code.replace('ref array<' + kind + '>', 'Array<' + kind + '>')
|
|
code = code.replace('protected ', '').replace('ref ERR_HQReport ', 'ERR_HQReport ')
|
|
code = re.sub(r'\b(ERR_HQReport|ERR_HQBelief|ERR_HQMemoryTrack) ([A-Za-z_]\w*)', r'\1* \2', code)
|
|
code = re.sub(r'class (\w+)\n\{', r'class \1\n{\npublic:', code)
|
|
code = re.sub(r'^}', '};', code, flags=re.M)
|
|
code = re.sub(r'\bvector\b', 'Vec', code)
|
|
for cls in ('ERR_HQMemoryRules', 'ERR_HQMemoryKind', 'Math', 'Vec'):
|
|
code = code.replace(cls + '.', cls + '::')
|
|
for name in ('report', 'last', 'belief', 'previous', 'track'):
|
|
code = re.sub(r'\b' + name + r'\.', name + '->', code)
|
|
code = re.sub(r'm_Tracks\[([^\]]+)\]\.', r'm_Tracks[\1]->', code)
|
|
code = code.replace('Latest().', 'Latest()->').replace('null', 'nullptr')
|
|
code = re.sub(r'^(\s+(?:IEntity|Vec|bool|float|int|string|ERR_HQReport\*|ERR_HQMemoryTrack\*) \w+);$',
|
|
r'\1{};', code, flags=re.M)
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <cmath>
|
|
#include <limits>
|
|
#include <algorithm>
|
|
#include <vector>
|
|
#include <string>
|
|
#include <type_traits>
|
|
using string=std::string;
|
|
struct Identity {int id;};
|
|
using IEntity=Identity*;
|
|
struct Vec {
|
|
float v[3]={0,0,0};Vec()=default;Vec(float x,float y,float z):v{x,y,z}{}
|
|
float& operator[](int i){return v[i];}float operator[](int i)const{return v[i];}
|
|
Vec operator+(Vec b)const{return {v[0]+b[0],v[1]+b[1],v[2]+b[2]};}
|
|
Vec operator-(Vec b)const{return {v[0]-b[0],v[1]-b[1],v[2]-b[2]};}
|
|
Vec operator*(float b)const{return {v[0]*b,v[1]*b,v[2]*b};}
|
|
Vec operator/(float b)const{return {v[0]/b,v[1]/b,v[2]/b};}
|
|
float Length()const{return std::sqrt(v[0]*v[0]+v[1]*v[1]+v[2]*v[2]);}
|
|
Vec Normalized()const{return *this/Length();}
|
|
static float DistanceXZ(Vec a,Vec b){return std::hypot(a[0]-b[0],a[2]-b[2]);}
|
|
};
|
|
template<class T>struct Array:std::vector<T>{
|
|
void Clear(){this->clear();}int Count(){return this->size();}
|
|
void Insert(T v){this->push_back(v);}void Remove(int i){assert(i>=0&&i<Count());this->erase(this->begin()+i);}
|
|
bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}
|
|
};
|
|
namespace Math {
|
|
template<class A,class B>auto Max(A a,B b){using T=std::common_type_t<A,B>;return std::max(T(a),T(b));}
|
|
template<class A,class B,class C>auto Clamp(A a,B b,C c){using T=std::common_type_t<A,B,C>;return std::clamp(T(a),T(b),T(c));}
|
|
float AbsFloat(float v){return std::abs(v);}float Pow(float a,float b){return std::pow(a,b);}
|
|
float Atan2(float a,float b){return std::atan2(a,b);}constexpr float RAD2DEG=57.295779513f;
|
|
}
|
|
struct ERR_HQReport {
|
|
int MessageId=0;IEntity Target=nullptr,Source=nullptr;Vec Position,Velocity;
|
|
float ObservedAt=-1;bool Distress=false,HasVelocity=false;string TravelMode="unknown";
|
|
ERR_HQReport* Snapshot(){return new ERR_HQReport(*this);}
|
|
};
|
|
''' + code + r'''
|
|
ERR_HQReport Report(int id,IEntity target,float when,Vec pos={0,0,0},string mode="on foot"){
|
|
ERR_HQReport r;r.MessageId=id;r.Target=target;r.Source=target;r.ObservedAt=when;r.Position=pos;r.TravelMode=mode;return r;
|
|
}
|
|
bool near(float a,float b,float tolerance=.001){return std::abs(a-b)<tolerance;}
|
|
int main(){
|
|
using Rules=ERR_HQMemoryRules;
|
|
float nan=std::numeric_limits<float>::quiet_NaN(),inf=std::numeric_limits<float>::infinity();
|
|
assert(near(Rules::Confidence(0),1)&&near(Rules::Confidence(600),.5)&&near(Rules::Confidence(1200),.25));
|
|
assert(Rules::Confidence(-1)==0&&Rules::Confidence(1,0)==0&&Rules::Confidence(nan)==0&&Rules::Confidence(inf)==0);
|
|
assert(near(Rules::Radius(0,1.2,25),25)&&near(Rules::Radius(600,1.2,25),745));
|
|
assert(Rules::Radius(1,nan,25)>2000&&!Rules::Actionable(.5,2001)&&!Rules::Actionable(nan,20));
|
|
assert(Rules::RadiusGrowth(ERR_HQMemoryKind::SIGHTING,"vehicle")>Rules::RadiusGrowth(ERR_HQMemoryKind::SIGHTING,"on foot"));
|
|
assert(!Rules::DeliveryFresh(10,9)&&!Rules::DeliveryFresh(-1,9)&&!Rules::DeliveryFresh(0,120001));
|
|
assert(Rules::DeliveryFresh(0,120000)&&!Rules::DeliveryFresh(nan,0));
|
|
assert(!Rules::PositionValid({nan,0,0})&&!Rules::PositionValid({0,inf,0})&&!Rules::PositionValid({1000001,0,0}));
|
|
assert(Rules::MotionCredible(10,60,"on foot","on foot"));
|
|
for(float gap:{0.f,1.f,121.f,nan})assert(!Rules::MotionCredible(gap,10,"on foot","on foot"));
|
|
assert(!Rules::MotionCredible(10,130,"on foot","on foot"));
|
|
assert(!Rules::MotionCredible(10,20,"unknown","unknown")&&!Rules::MotionCredible(10,20,"on foot","vehicle"));
|
|
assert(near(Rules::Predict({0,40,0},{100,10,0},100)[0],300));
|
|
assert(near(Rules::Predict({0,40,0},{1,10,0},100)[0],30)&&near(Rules::Predict({0,40,0},{1,10,0},100)[1],40));
|
|
Identity a{1},b{2},friendly{3};ERR_HQMemory memory;
|
|
auto first=Report(1,&a,1000,{10,40,10});first.HasVelocity=true;first.Velocity={100,0,100};
|
|
assert(memory.Ingest(&first,2000));auto belief=memory.Get(&a);int beliefId=belief->BeliefId;
|
|
assert(belief->BeliefRevision==1&&belief->SampleCount==1&&!belief->HasVelocity&&belief->EvaluatedAt==2000);
|
|
first.Position={999,999,999};first.TravelMode="aircraft";belief->Position={777,0,777};belief->Report->Position={888,0,888};
|
|
assert(near(memory.Get(&a)->Position[0],10)&&memory.Get(&a)->TravelMode=="on foot");
|
|
auto next=Report(2,&a,11000,{70,80,10});assert(memory.Ingest(&next,16000));belief=memory.GetById(beliefId);
|
|
assert(belief->BeliefRevision==2&&belief->HasVelocity&&near(belief->Speed,6)&&near(belief->HeadingDegrees,90));
|
|
assert(near(belief->PredictedPosition[0],100)&&near(belief->PredictedPosition[1],80));
|
|
memory.Tick(611000);belief=memory.GetById(beliefId);
|
|
assert(belief&&near(belief->Confidence,.5)&&near(belief->Radius,745)&&near(belief->PredictedPosition[0],250));
|
|
assert(belief->PredictionSeconds==30&&belief->Actionable());
|
|
memory.Tick(610000);assert(memory.Get(&a)->EvaluatedAt==611000);memory.Tick(nan);assert(memory.Get(&a)->EvaluatedAt==611000);
|
|
assert(!memory.Ingest(&next,16000)&&!memory.Ingest(&next,611000));
|
|
assert(memory.NegativeSearch(beliefId,2,611000));belief=memory.Get(&a);
|
|
assert(near(belief->Confidence,.325)&&belief->ObservedAt==11000&&belief->SampleCount==2&&belief->BeliefRevision==2);
|
|
assert(!memory.NegativeSearch(beliefId,2,611000)&&!memory.NegativeSearch(beliefId,1,611000));
|
|
auto renewed=Report(3,&a,611000,{100,80,10});assert(memory.Ingest(&renewed,611000));
|
|
assert(near(memory.Get(&a)->Confidence,1)&&memory.Get(&a)->BeliefRevision==3&&!memory.Get(&a)->HasVelocity);
|
|
assert(!memory.NegativeSearch(beliefId,2,611000));
|
|
// Per-kind identity protects a moving contact from uncertain friendly/body information.
|
|
auto distress=Report(4,&a,611000,{500,0,500});distress.Distress=true;distress.Source=&friendly;
|
|
assert(memory.Ingest(&distress,612000)&&memory.Count()==2&&memory.Get(&a)->Kind==ERR_HQMemoryKind::SIGHTING);
|
|
auto distressBelief=memory.At(1);assert(distressBelief->Target==nullptr&&!distressBelief->HasVelocity&&!distressBelief->Actionable());
|
|
auto body=Report(5,&a,612000,{100,0,10},"body");assert(memory.Ingest(&body,612000));
|
|
assert(memory.At(2)->Kind==ERR_HQMemoryKind::BODY&&!memory.At(2)->HasVelocity&&memory.Get(&a)->BeliefRevision==3);
|
|
assert(!memory.NegativeSearch(memory.At(2)->BeliefId,1,612000));
|
|
// A deleted engine identity cannot erase or rebind an already numbered belief.
|
|
memory.m_Tracks[0]->Key=nullptr;memory.m_Tracks[0]->Latest()->Target=nullptr;
|
|
assert(memory.GetById(beliefId)&&near(memory.GetById(beliefId)->Position[0],100));
|
|
memory.Tick(2300000);assert(memory.Count()==0&&!memory.GetById(beliefId));
|
|
memory.Clear();auto restart=Report(1,&a,0);assert(memory.Ingest(&restart,0));assert(memory.Get(&a)->BeliefId>beliefId);
|
|
// Delayed legitimate deliveries for another track survive arbitrarily high fresh IDs.
|
|
memory.Clear();auto high=Report(100000,&a,10000);assert(memory.Ingest(&high,11000));
|
|
auto delayed=Report(2,&b,9000);assert(memory.Ingest(&delayed,12000));
|
|
auto replay=delayed;replay.ObservedAt=12000;replay.Position={500,0,0};assert(!memory.Ingest(&replay,12000));
|
|
auto stale=Report(200000,&a,0);assert(!memory.Ingest(&stale,130001));
|
|
auto low=Report(3,&b,130001);assert(memory.Ingest(&low,130001));
|
|
auto future=Report(999999,&a,130003);assert(!memory.Ingest(&future,130002));
|
|
auto good=Report(4,&b,130002);assert(memory.Ingest(&good,130002));
|
|
// Strictly chronological samples, limited history and conservative discontinuities.
|
|
memory.Clear();for(int i=1;i<=12;i++){auto r=Report(i,&a,i*2000,{float(i*2),0,0});assert(memory.Ingest(&r,i*2000));}
|
|
assert(memory.Get(&a)->SampleCount==8&&memory.Get(&a)->BeliefRevision==12&&memory.Get(&a)->HasVelocity);
|
|
auto old=Report(13,&a,23000);assert(!memory.Ingest(&old,25000)&&memory.Get(&a)->BeliefRevision==12);
|
|
auto jump=Report(14,&a,26000,{10000,0,0});assert(memory.Ingest(&jump,26000)&&!memory.Get(&a)->HasVelocity);
|
|
// A near-simultaneous second observer must not erase credible motion
|
|
// history. Use the nearest sample at least two seconds old, never a live
|
|
// entity position or an older convenient sample across a mode change.
|
|
memory.Clear();
|
|
auto spaced=Report(1,&a,1000,{10,0,20});assert(memory.Ingest(&spaced,1000));
|
|
auto closeA=Report(2,&a,4000,{10,0,20});assert(memory.Ingest(&closeA,4000));
|
|
auto closeB=Report(3,&a,4500,{10,0,20});assert(memory.Ingest(&closeB,4500));
|
|
assert(memory.Get(&a)->HasVelocity&&near(memory.Get(&a)->Speed,0));
|
|
auto moving=Report(4,&a,5000,{18,0,20});assert(memory.Ingest(&moving,5000));
|
|
assert(memory.Get(&a)->HasVelocity&&near(memory.Get(&a)->Speed,2));
|
|
auto boarded=Report(5,&a,5500,{18,0,20},"vehicle");assert(memory.Ingest(&boarded,5500));
|
|
assert(!memory.Get(&a)->HasVelocity);
|
|
auto dismounted=Report(6,&a,6000,{18,0,20});assert(memory.Ingest(&dismounted,6000));
|
|
assert(!memory.Get(&a)->HasVelocity); // do not bridge the intervening vehicle observation
|
|
auto farJump=Report(7,&a,8000,{10000,0,0});assert(memory.Ingest(&farJump,8000));
|
|
assert(!memory.Get(&a)->HasVelocity); // do not skip an implausible eligible pair
|
|
memory.Clear();
|
|
auto rapidA=Report(1,&a,1000);assert(memory.Ingest(&rapidA,1000));
|
|
auto rapidB=Report(2,&a,1500);assert(memory.Ingest(&rapidB,1500));
|
|
assert(!memory.Get(&a)->HasVelocity); // insufficient time baseline still refuses motion
|
|
// No equal/weak burst can evict a stronger current sighting; later stronger reports can.
|
|
memory.Clear();Identity people[70];for(int i=0;i<70;i++)people[i].id=i+10;
|
|
for(int i=0;i<64;i++){auto r=Report(i+1,&people[i],0);assert(memory.Ingest(&r,0));}
|
|
auto over=Report(65,&people[64],0);assert(!memory.Ingest(&over,0)&&memory.Count()==64);
|
|
over.ObservedAt=10000;assert(memory.Ingest(&over,10000)&&memory.Count()==64);
|
|
auto weak=Report(66,&people[65],10000);weak.Distress=true;assert(!memory.Ingest(&weak,10000));
|
|
assert(!memory.At(-1)&&!memory.At(64)&&!memory.Get(nullptr));
|
|
// Heard fire is source-keyed, imprecise, short-lived, never a precision target.
|
|
memory.Clear();auto sound=Report(1,nullptr,1000,{75,0,225},"heard gunfire");sound.Source=&friendly;
|
|
assert(memory.Ingest(&sound,1000));auto noise=memory.At(0);
|
|
assert(noise->Kind==ERR_HQMemoryKind::GUNFIRE&&!noise->Target&&!noise->HasVelocity&&!noise->Actionable());
|
|
assert(near(noise->Radius,150)&&near(noise->Confidence,.4));
|
|
int soundId=noise->BeliefId;assert(memory.NegativeSearch(soundId,1,1000));
|
|
assert(near(memory.GetById(soundId)->Confidence,.26));
|
|
sound.MessageId=2;sound.ObservedAt=3000;sound.Position={375,0,225};assert(memory.Ingest(&sound,3000));
|
|
assert(memory.Count()==1&&!memory.At(0)->HasVelocity&&memory.At(0)->Target==nullptr);
|
|
auto visual=Report(3,&a,3000,{400,0,210});assert(memory.Ingest(&visual,3000));
|
|
assert(memory.Count()==2&&memory.Get(&a)->Actionable()&&near(memory.Get(&a)->Confidence,1));
|
|
memory.Tick(63000);assert(near(memory.GetById(soundId)->Confidence,.2));
|
|
// The global replay table is bounded even when valid observation IDs arrive rapidly.
|
|
memory.Clear();for(int i=1;i<=4096;i++){auto r=Report(i,&a,float(i));assert(memory.Ingest(&r,float(i)));}
|
|
auto full=Report(4097,&a,4097);assert(!memory.Ingest(&full,4097)&&memory.m_Seen.Count()==4096);
|
|
memory.Tick(124096);full.ObservedAt=124096;assert(memory.Ingest(&full,124096)&&memory.m_Seen.Count()==2);
|
|
auto expired=Report(1,&b,1);assert(!memory.Ingest(&expired,124096));
|
|
}
|
|
''')
|
|
print('PASS: actual HQ memory rules/service preserve delivered snapshots, chronology, replay bounds, decay, motion limits, identity and negative-search revisions')
|