Files

93 lines
9.2 KiB
Python

"""Actual local handoff predicate/history/network route/origin delivery state machine."""
from pathlib import Path
import os,re
from policy_harness import ROOT,body,run
network=Path(os.environ.get('ERR_NETWORK_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQNetwork.c'))
helper=Path(os.environ.get('ERR_HANDOFF_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQLocalHandoff.c'))
# Reuse the existing native graph syntax translation, not its policy doubles.
base=Path(os.environ.get('ERR_NETWORK_HARNESS',ROOT/'tests/hq_network_regression.py')).read_text()
ns={'__file__':str(ROOT/'tests/hq_network_regression.py')}
exec(base[:base.index("headers=r'''")],ns)
base_trans=ns['trans']
def trans(s):
s=base_trans(s).replace('ref ','')
s=s.replace('ERR_GameMode.Get()', 'ERR_GameMode::Get()').replace('ERR_EnemyEquipmentRules.','ERR_EnemyEquipmentRules::').replace('ERR_ForceRules.','ERR_ForceRules::').replace('ERR_HQLocalHandoff.','ERR_HQLocalHandoff::')
s=re.sub(r'\bERR_GameMode (\w+)',r'ERR_GameMode* \1',s)
s=re.sub(r'\bERR_HQStatusSampler (\w+)',r'ERR_HQStatusSampler* \1',s)
s=s.replace('array<IEntity> busy','Arr<IEntity*>* busy').replace('array<IEntity>','Arr<IEntity*>').replace('array<float>','Arr<float>')
s=s.replace('foreach (ERR_HQRadioNode* node : m_Nodes)','for (ERR_HQRadioNode* node : m_Nodes)')
for name in ('roster','receiver','witness','busy'):
s=s.replace(name+'.',name+'->')
s=s.replace('receiver->Carrier.GetOrigin()', 'receiver->Carrier->GetOrigin()')
return s
headers=r'''
#include <cassert>
#include <cmath>
#include <algorithm>
#include <vector>
#include <map>
#include <string>
#include <limits>
struct vector{float v[3]{};float operator[](int i)const{return v[i];}static float DistanceXZ(vector a,vector b){return std::hypot(a[0]-b[0],a[2]-b[2]);}static float Distance(vector a,vector b){return std::sqrt(std::pow(a[0]-b[0],2)+std::pow(a[1]-b[1],2)+std::pow(a[2]-b[2],2));}};
vector V(float x,float z=0,float y=0){return {{x,y,z}};}
namespace Math{int Floor(float x){return std::floor(x);}}
std::string Format(const char*,int x,int z){return std::to_string(x)+":"+std::to_string(z);}
template<class T>struct Arr:std::vector<T>{using std::vector<T>::vector;using std::vector<T>::operator=;int Count(){return this->size();}bool IsEmpty(){return this->empty();}void Clear(){this->clear();}void Insert(T x){this->push_back(x);}void InsertAt(T x,int i){this->insert(this->begin()+i,x);}void Remove(int i){this->erase(this->begin()+i);}int Find(T x){auto it=std::find(this->begin(),this->end(),x);return it==this->end()?-1:int(it-this->begin());}bool Contains(T x){return Find(x)>=0;}};
template<class K,class V>struct Map:std::map<K,V>{void Clear(){this->clear();}bool Contains(K k){return this->count(k);}bool Find(K k,V& v){if(!Contains(k))return false;v=this->at(k);return true;}V Get(K k){if(!Contains(k))return {};return this->at(k);}void Insert(K k,V v){(*this)[k]=v;}};
struct SCR_AIGroup{bool enemy=true,military=true;int players=0;};
struct IEntity{vector pos;bool alive=true,captured=false,player=false;int radio=0;SCR_AIGroup* group=nullptr;vector GetOrigin(){return pos;}};
struct ERR_GameMode{static ERR_GameMode*Get(){static ERR_GameMode m;return &m;}};
struct ERR_EnemyEquipmentRules{static bool Eligible(ERR_GameMode* m,SCR_AIGroup* g,IEntity* a){return m&&g&&g->enemy&&g->military&&g->players==0&&a&&a->alive&&!a->captured&&!a->player&&a->group==g;}};
struct ERR_ForceRules{static bool Finite(float x){return std::isfinite(x);}};
struct ERR_HQRadioNode{IEntity* Carrier{};SCR_AIGroup* Group{};int UnitId{},Capability{};bool Valid(bool manpack=false){return ERR_EnemyEquipmentRules::Eligible(ERR_GameMode::Get(),Group,Carrier)&&Carrier->radio>=(manpack?2:1);}std::string Label(){return "unit "+std::to_string(UnitId);}};
struct ERR_HQStatusSampler{SCR_AIGroup*group=nullptr;Arr<IEntity*>members;bool attributed=true;bool OwnsRoster(SCR_AIGroup*g){return group&&group==g;}int InitialStrength(){return members.Count();}IEntity*InitialMember(int i){return members[i];}bool CanAttribute(int i,IEntity*a){return attributed&&members[i]==a;}};
struct ERR_HQJournal{Arr<std::string> events;void Write(std::string kind,std::string text,int){events.Insert(kind+" "+text);}};
'''
helper_code=trans(helper.read_text()).replace('class ERR_HQLocalHandoff','struct ERR_HQLocalHandoff').replace('class ERR_HQObservationHistory','struct ERR_HQObservationHistory').replace('\n}\n','\n};\n')
route_fields='SCR_AIGroup* LocalSource=nullptr;IEntity* LocalWitness=nullptr;ERR_HQStatusSampler* LocalRoster=nullptr;Arr<ERR_HQRadioNode*> Nodes;bool Downlink{},Sending{},Failed{},Originated{};int Cursor{},MessageId{};float Due{},StartedAt=-1,OriginDue=-1;vector SentFrom;std::string Payload;'
route='struct ERR_HQRoute{'+route_fields+'\n'+'\n'.join(trans(body(network,x)) for x in ['int Advance(', 'protected string SenderLabel(', 'protected int Fail('])+'};\n'
network_fields='int m_LocalChecks=0;Arr<ERR_HQRadioNode*> m_Nodes;Arr<int> m_Parents,m_Depths;Map<IEntity*,int> m_Indices;Map<std::string,Arr<int>*> m_Cells;bool Overflow{};'
graph='struct Network{'+network_fields+'\n'+'\n'.join(trans(body(network,x)) for x in ['void Clear(', 'protected string Cell(', 'void Add(', 'void Build(', 'ERR_HQRoute Route(', 'ERR_HQRoute LocalRoute('])+'};\n'
run(headers+helper_code+route+graph+r'''
struct Fixture{SCR_AIGroup source,receiver;IEntity witness,caller,other;ERR_HQStatusSampler roster;Network network;Arr<IEntity*>busy;
Fixture(){witness.group=&source;caller.group=&receiver;caller.radio=2;caller.pos=V(100);roster.group=&source;roster.members={&witness};network.Add(&caller,&receiver,2,2);network.Build();}
ERR_HQRoute* Local(){return network.LocalRoute(&source,&roster,&busy);}
};
int main(){
{Fixture f;auto route=f.Local();assert(route&&route->Nodes[0]->Carrier==&f.caller&&route->LocalSource==&f.source&&route->LocalWitness==&f.witness&&route->LocalRoster==&f.roster);
ERR_HQJournal journal;route->MessageId=17;route->Payload="unchanged sighting";assert(route->Advance(0,&journal)==0&&!route->Originated&&!route->Sending);
assert(route->Advance(2999,&journal)==0&&!route->Originated);assert(route->Advance(3000,&journal)==0&&route->Originated&&route->Sending);
// Local information is already handed over; later witness loss cannot erase it.
f.witness.alive=false;assert(route->Advance(5000,&journal)==1&&route->MessageId==17&&route->Payload=="unchanged sighting");}
for(int reason=0;reason<13;reason++){
Fixture f;auto r=f.Local();ERR_HQJournal j;assert(r&&r->Advance(0,&j)==0);
if(reason==0){f.witness.alive=false;}if(reason==1){f.witness.group=&f.receiver;}
if(reason==2){f.witness.player=true;}if(reason==3){f.roster.attributed=false;}
if(reason==4){f.caller.radio=0;}if(reason==5){f.caller.alive=false;}
if(reason==6){f.caller.captured=true;}if(reason==7){f.caller.group=&f.source;}
if(reason==8){f.caller.pos=V(100.01f);}if(reason==9){f.caller.pos=V(0,0,100.01f);}
if(reason==10){r->LocalSource=nullptr;}if(reason==11){r->LocalWitness=nullptr;}
if(reason==12){f.source.players=1;}
assert(r->Advance(1000,&j)==-1&&r->Failed&&!r->Originated&&!r->Sending);assert(r->Advance(5000,&j)==-1);
}
// Never trade an invalid first member for an unowned replacement; a different original may hand over.
{Fixture f;f.roster.members[0]=&f.other;assert(!f.Local());f.other.group=&f.source;f.roster.members.Insert(&f.witness);assert(f.Local());}
{Fixture f;f.busy.Insert(&f.caller);assert(!f.Local());f.busy.Clear();assert(f.Local());}
{Fixture f;f.caller.radio=1;f.network.Clear();f.network.Add(&f.caller,&f.receiver,2,1);f.network.Build();assert(!f.Local());}
{Fixture f;f.roster.members.resize(33,&f.witness);assert(!f.Local());}
{Fixture f;f.network.m_LocalChecks=4096;assert(!f.Local());f.network.Clear();f.network.Add(&f.caller,&f.receiver,2,2);f.network.Build();assert(f.Local());}
{Fixture f;f.network.Overflow=true;assert(!f.Local());}
// Direct routes remain unaffected and final radio loss is still terminal after local completion.
{Fixture f;auto direct=f.network.Route(&f.caller);f.witness.alive=false;ERR_HQJournal j;assert(direct->Advance(0,&j)==0);assert(direct->Advance(3000,&j)==0);assert(direct->Advance(5000,&j)==1);}
{Fixture f;auto r=f.Local();ERR_HQJournal j;r->Advance(0,&j);r->Advance(3000,&j);f.caller.radio=0;assert(r->Advance(5000,&j)==-1);}
// Per-source watermark rejects duplicate/replayed/future/stale sightings including failed-route attempts.
{ERR_HQObservationHistory history;IEntity target;assert(history.CanQueue(&target,100,100));history.Queued(&target,100,100);
assert(!history.CanQueue(&target,100,101)&&!history.CanQueue(&target,99,101));assert(history.CanQueue(&target,101,101));
assert(!history.CanQueue(&target,102,101)&&history.CanQueue(&target,101,602)&&history.CanQueue(&target,101,30101)&&!history.CanQueue(&target,101,30102));
assert(!history.CanQueue(&target,std::numeric_limits<float>::quiet_NaN(),602));}
{ERR_HQObservationHistory history;IEntity targets[65];for(int i=0;i<64;i++){history.Queued(&targets[i],100,100);}assert(!history.CanQueue(&targets[64],100,100));
assert(history.CanQueue(&targets[64],120101,120101));assert(!history.CanQueue(&targets[0],100,120101));}
}
''')
print('PASS: actual original-roster local handoff,100m3D bound,busy carrier/budget exclusion,origin interruption including deletedsource,normal radio delivery and per-source replay history')