Files
EscapistsReintroduced/tests/test_hq_memory.py
T
jakub cab998fb64 Combine Workshop addon and fix prison startup
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.
2026-09-20 16:42:52 +02:00

150 lines
10 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);
// 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));
// 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')