175 lines
8.9 KiB
Python
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')
|