Files
EscapistsReintroduced/tests/test_hq_coverage.py
T

175 lines
8.9 KiB
Python

"""Actual bounded negative-search coverage rules; physical radio delivery is HQ's boundary."""
from policy_harness import ROOT, body, run
P=ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQCoverage.c'
PURSUIT=ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQPursuit.c'
def cpp(s):
return (s.replace('protected ', '').replace('Math.', 'Math::').replace('vector.', 'vector::')
.replace('ERR_HQPursuitRules.', 'ERR_HQPursuitRules::').replace('ERR_HQCoverageRules.', 'ERR_HQCoverageRules::')
.replace('out vector ', 'vector& ').replace('array<vector> reserved', 'Array<vector>* reserved')
.replace('ERR_HQSearchReport report', 'ERR_HQSearchReport* report').replace('report.', 'report->')
.replace('reserved.Count()', 'reserved->Count()').replace('= null', '= nullptr')
.replace('foreach (vector position : reserved)', 'for (vector position : *reserved)')
.replace('foreach (int known, float searched : m_Cells)', 'for (auto [known, searched] : m_Cells)')
.replace('int oldestKey;', 'int oldestKey=0;').replace('bool found;', 'bool found=false;'))
common='\n'.join(cpp(body(PURSUIT,s)) for s in ('static bool Finite(', 'static bool PositionValid('))
rules='\n'.join(cpp(body(P,s)) for s in ('static bool PositionValid(', 'static int Cell(', 'static int Key(',
'static bool Recent(', 'static bool ValidReport(', 'static bool Point('))
methods='\n'.join(cpp(body(P,s)) for s in ('int Count(', 'void Forget(', 'bool Recent(vector', 'bool Ingest(',
'protected bool Reserved(', 'bool Select(', 'void Clear('))
run(r'''
#include <cassert>
#include <cmath>
#include <algorithm>
#include <limits>
#include <map>
#include <vector>
struct vector{
float x=0,y=0,z=0;
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;}
static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}
};
vector Vector(float x,float y,float z){return {x,y,z};}
struct Math{
static float Clamp(float n,float lo,float hi){return std::clamp(n,lo,hi);}
static float Floor(float n){return std::floor(n);}
static float AbsFloat(float n){return std::abs(n);}
};
template<class T>struct Array:std::vector<T>{int Count(){return this->size();}};
template<class K,class V>struct Map:std::map<K,V>{
bool Contains(K k){return this->count(k);}V Get(K k){return this->count(k)?this->at(k):V{};}
int Count(){return this->size();}void Set(K k,V v){(*this)[k]=v;}
void Remove(K k){this->erase(k);}void Clear(){this->clear();}
};
struct ERR_HQPursuitRules{
'''+common+r'''
};
struct ERR_HQCoverageRules{
'''+rules+r'''
};
struct ERR_HQSearchReport{
int SourceId=1,MessageId=1,BeliefId=1,BeliefRevision=1;
vector Position{250,12,250};float ObservedAt=0;
bool SweepCompleted=true,ContactFound=false;
};
struct Coverage{
Map<int,float>m_Cells;Map<int,int>m_Messages;
'''+methods+r'''
};
int main(){
using R=ERR_HQCoverageRules;
assert(R::Cell(0)==0&&R::Cell(499.9)==0&&R::Cell(500)==1);
assert(R::Cell(-.1)==-1&&R::Cell(-500)==-1&&R::Cell(-500.1)==-2);
assert(R::Key({-1,0,0})!=R::Key({0,0,-1}));
assert(R::Recent(0,900000)&&!R::Recent(0,900001)&&!R::Recent(1,0));
assert(R::ValidReport(1,1,1,1,true,false,0,120000));
assert(!R::ValidReport(1,1,1,1,true,false,0,120001));
assert(!R::ValidReport(1,1,1,1,false,false,0,1000));
assert(!R::ValidReport(1,1,1,1,true,true,0,1000));
assert(!R::ValidReport(1,1,0,1,true,false,0,1000));
assert(!R::ValidReport(1,1,1,0,true,false,0,1000));
float nan=std::numeric_limits<float>::quiet_NaN();
assert(!R::ValidReport(1,1,1,1,true,false,nan,1000));
assert(!R::PositionValid({nan,0,0})&&!R::PositionValid({1000000,0,0}));
vector point;
assert(R::Point(0,0,{0,12,0},0,point)&&point.x==0&&point.z==0);
assert(R::Point(0,0,{180,12,180},25,point)&&point.x==180&&point.z==180);
assert(!R::Point(2,2,{180,12,180},25,point));
// Undelivered reports remain inert objects. Only Ingest applies the delivered snapshot.
Coverage coverage;ERR_HQSearchReport report;
assert(coverage.Count()==0&&!coverage.Recent(report.Position,1000));
assert(coverage.Ingest(&report,1000)&&coverage.Count()==1);
assert(coverage.Recent(report.Position,900000)&&!coverage.Recent(report.Position,900001));
assert(!coverage.Ingest(&report,1000));
report.Position={750,12,250};report.BeliefRevision=2;
assert(!coverage.Ingest(&report,1000)&&!coverage.Recent(report.Position,1000));
report.MessageId=2;report.ObservedAt=1000;
assert(coverage.Ingest(&report,2000)&&coverage.Count()==2);
report.MessageId=1;report.Position={1250,12,250};
assert(!coverage.Ingest(&report,2000));
// Invalid/future/old/incomplete/positive observations never advance replay guards.
ERR_HQSearchReport invalid;invalid.SourceId=2;
for(int field=0;field<8;field++){
invalid=ERR_HQSearchReport{};invalid.SourceId=2;
if(field==0)invalid.ObservedAt=2001;
if(field==1)invalid.SweepCompleted=false;
if(field==2)invalid.ContactFound=true;
if(field==3)invalid.BeliefId=0;
if(field==4)invalid.BeliefRevision=0;
if(field==5)invalid.Position.x=nan;
if(field==6)invalid.MessageId=0;
if(field==7)invalid.ObservedAt=-1;
assert(!coverage.Ingest(&invalid,2000)&&!coverage.m_Messages.Contains(2));
}
assert(!coverage.Ingest(nullptr,2000));
// A fresh positive sighting may reopen a coarse cell, without replaying an old negative.
Coverage reopened;ERR_HQSearchReport searched;
assert(reopened.Ingest(&searched,1000));
reopened.Forget({1,12,1});assert(reopened.Count()==0);
assert(!reopened.Ingest(&searched,1000));
searched.MessageId=2;searched.ObservedAt=2000;
assert(reopened.Ingest(&searched,2000));
reopened.Forget({nan,0,0});assert(reopened.Count()==1);
// Delivery latency does not restart the 900-second coverage lifetime.
Coverage delayed;ERR_HQSearchReport late;
assert(delayed.Ingest(&late,120000));
assert(delayed.Recent(late.Position,900000)&&!delayed.Recent(late.Position,900001));
Coverage old;assert(!old.Ingest(&late,120001)&&old.Count()==0);
// Bounded cell retention evicts the oldest only for genuinely newer coverage.
Coverage bounded;ERR_HQSearchReport item;
for(int i=0;i<128;i++){
item.MessageId=i+1;item.Position={float(i*500+250),12,250};item.ObservedAt=1000+i;
assert(bounded.Ingest(&item,2000));
}
assert(bounded.Count()==128);
item.MessageId=129;item.Position={70000,12,250};item.ObservedAt=999;
assert(!bounded.Ingest(&item,2000)&&bounded.Count()==128);
item.MessageId=130;item.ObservedAt=1500;
assert(bounded.Ingest(&item,2000)&&bounded.Count()==128);
assert(!bounded.Recent({250,12,250},2000)&&bounded.Recent(item.Position,2000));
// Replay watermarks do not grow or evict an old source's identity under pressure.
Coverage sources;
for(int i=1;i<=128;i++){
item=ERR_HQSearchReport{};item.SourceId=i;item.ObservedAt=i;
assert(sources.Ingest(&item,1000));
}
item.SourceId=129;item.ObservedAt=129;
assert(!sources.Ingest(&item,1000)&&sources.m_Messages.Count()==128);
item.SourceId=1;item.MessageId=2;item.ObservedAt=130;
assert(sources.Ingest(&item,1000)&&sources.m_Messages.Count()==128);
// Nearest unswept candidates remain inside uncertainty, including a small circle.
Coverage search;vector center{250,12,250},destination;
assert(search.Select(center,25,{1000,0,250},1000,destination));
assert(R::Key(destination)==R::Key(center)&&vector::DistanceXZ(center,destination)<=25);
item=ERR_HQSearchReport{};assert(search.Ingest(&item,1000));
assert(!search.Select(center,25,{1000,0,250},1000,destination));
assert(search.Select(center,25,{1000,0,250},900001,destination));
assert(search.Select(center,600,{1000,0,250},1000,destination));
assert(destination.x==750&&destination.z==250);
Array<vector> reserved;reserved.push_back({501,0,1}); // different point, same reserved cell
assert(search.Select(center,600,{1000,0,250},1000,destination,&reserved));
assert(R::Key(destination)!=R::Key(reserved[0]));
assert(vector::DistanceXZ(center,destination)<=600);
Coverage tiny;reserved.clear();reserved.push_back({1,0,1});
assert(!tiny.Select(center,25,center,1000,destination,&reserved));
for(int i=0;i<7;i++)reserved.push_back(center);
assert(reserved.Count()==8&&tiny.Select(center,2000,center,1000,destination,&reserved));
reserved.push_back(center);
assert(!tiny.Select(center,2000,center,1000,destination,&reserved));
assert(!tiny.Select(center,2001,center,1000,destination));
assert(!tiny.Select(center,-1,center,1000,destination));
assert(!tiny.Select(center,nan,center,1000,destination));
assert(!tiny.Select(center,100,center,-1,destination));
assert(!tiny.Select({nan,0,0},100,center,1000,destination));
assert(!tiny.Select(center,100,{nan,0,0},1000,destination));
sources.Clear();assert(sources.Count()==0&&sources.m_Messages.Count()==0);
}
''')
source=P.read_text()
for forbidden in ('GetGame(', 'GetOrigin(', 'GetPlayers(', 'GetControlledEntity(', 'Spawn(', 'CallLater(', 'IEntity'):
assert forbidden not in source
assert all(len(line)<120 for line in source.splitlines())
print('PASS: actual bounded delivered-negative coverage, replay/freshness/expiry/invalid guards, oldest eviction, small-circle and nearest-unswept selection, reserved-cell exclusion and cleanup')