Files
EscapistsReintroduced/tests/test_hq_uplink_fairness.py
T

147 lines
10 KiB
Python

"""Actual HQ report methods, with deterministic radio/perception boundary doubles.
Native radio and perception remain explicit boundary doubles.
"""
from pathlib import Path
import os,re
from policy_harness import ROOT,body,run
P=Path(os.environ.get('ERR_HQ_SOURCE',str(ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQ.c')))
original=(ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQ.c').read_text()
legacy='Observe(unit, now);\n ObserveBody(unit, now);\n StatusReport(unit, now);\n Sweep(unit, now);'
assert legacy in original or 'QueueReports(unit, now);' in original
def cpp(s):
s=s.replace('protected ','').replace('array<ref ERR_HQVisualContact>','Array<ERR_HQVisualContact*>')
for t in ('ERR_HQAmmoSnapshot','ERR_HQUnit','ERR_HQVisualContact','ERR_HQRoute','ERR_HQReport','ERR_HQStatusReport','ERR_HQTask','ERR_HQSearchReport'):
s=re.sub(r'\b'+t+r' (?=\w)',t+'* ',s)
s=s.replace('ERR_HQReport* report;','ERR_HQReport* report = nullptr;')
for v in ('unit','route','report','seen','task','stock'):
s=s.replace(v+'.',v+'->')
for v in ('Utility','ReportSelection','StatusSampler','Group'):
s=s.replace('->'+v+'.','->'+v+'->')
s=s.replace('m_Mode.Settings().Get(', 'm_Mode->Settings()->Get(')
s=s.replace('ERR_HQPursuitRules.', 'ERR_HQPursuitRules::')
s=s.replace('ERR_HQObservation.','ERR_HQObservation::').replace('vector.DistanceXZ','vector::DistanceXZ').replace('string.Format','Format')
return s.replace('AmmoState.', 'AmmoState->')
policy_path=Path(os.environ.get('ERR_AMMO_REPORTING_ROOT', ROOT))/'addons/ai/Scripts/Game/ERR/ERR_HQAmmoReporting.c'
policy=cpp(policy_path.read_text()).replace('class ERR_HQAmmoReporting','struct ERR_HQAmmoReporting').rstrip()+';'
methods='\n'.join(cpp(body(P,s)) for s in ('protected void Observe(', 'protected void ObserveBody(', 'protected void StatusReport(', 'protected void Sweep(', 'protected void QueueReports('))
run(r'''
#include <cassert>
#include <cmath>
#include <vector>
#include <string>
using string=std::string;
template<class... T>string Format(T... args){return "diagnostic";}
struct vector{float x=0,z=0;static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}};
template<class T>struct Array:std::vector<T>{bool IsEmpty(){return this->empty();}};
struct Group{vector position;vector GetOrigin(){return position;}};
struct Settings{int interval=5;int Get(string){return interval;}};
struct Mode{struct Settings settings;struct Settings* Settings(){return &settings;}};
struct ERR_HQVisualContact{void* Entity=this;vector Position;float ObservedAt=0;};
struct Utility{bool visible=true;};
struct ERR_HQObservation{static void Gather(Utility* u,float now,Array<ERR_HQVisualContact*>& a){
static ERR_HQVisualContact contact;if(u->visible){contact.ObservedAt=now;a.push_back(&contact);}}
template<class T>static void Describe(T*,Array<ERR_HQVisualContact*>&,float){}
};
struct Selection{int attempts=0;ERR_HQVisualContact* Select(Array<ERR_HQVisualContact*>& a){return a.front();}void Queued(void*){attempts++;}};
struct ERR_HQReport{int MessageId=0;void* Target=nullptr;Group* Source=nullptr;vector Position;float ObservedAt=0;
string Description(float){return "contact";}ERR_HQReport* Snapshot(){return new ERR_HQReport(*this);}};
struct ERR_HQPursuitRules{static bool Finite(float value){return std::isfinite(value);}};
struct ERR_HQAmmoSnapshot{bool Complete=false;int SmallArmsRounds=0,SmallArmsChambers=0,SmallArmsMuzzles=0,Members=0;bool Valid(int){return false;}};
struct ERR_HQStatusReport{ERR_HQAmmoSnapshot* AmmoState=nullptr;int MessageId=0,Count=4,Fit=4,ConfirmedLosses=0;string SourceId="own-1";float ObservedAt=0;};
'''+policy+r'''
struct Sampler{int observations=0,samples=0;bool valid=true;void ObserveDeaths(){observations++;}
ERR_HQStatusReport* Sample(float now,int id,void*){samples++;if(!valid)return nullptr;auto r=new ERR_HQStatusReport;r->MessageId=id;r->ObservedAt=now;return r;}};
struct ERR_HQSearchReport{int SourceId=0,MessageId=0,WatchMessageId=0,WatchRequestId=0,BeliefId=0,BeliefRevision=0;
string WatchSourceId;vector Position;float ObservedAt=0;bool SweepCompleted=false,ContactFound=false;};
struct ERR_HQTask{bool SweepQueued=false;float ArrivedAt=-1;vector Destination;
string WatchSourceId;int WatchMessageId=0,WatchRequestId=0,BeliefId=1,BeliefRevision=2;};
struct ERR_HQRoute{int MessageId=0;string Payload;};
struct Network{bool available=true;int routes=0;ERR_HQRoute* Route(void*){routes++;return available?new ERR_HQRoute:nullptr;}};
struct Bodies{bool visible=false;bool Observe(Group*,float now,ERR_HQReport*& r){if(!visible)return false;r=new ERR_HQReport;r->ObservedAt=now;return true;}};
struct Journal{int seq=0;int Message(){return ++seq;}void Write(string,string,int){}};
struct ERR_HQUnit{int Id=1;struct Group ownGroup;struct Utility ownUtility;Selection selection;Sampler sampler;
struct Group* Group=&ownGroup;struct Utility* Utility=&ownUtility;void* Radio=this;
Selection* ReportSelection=&selection;Sampler* StatusSampler=&sampler;
ERR_HQRoute* Uplink=nullptr;ERR_HQReport* PendingReport=nullptr;ERR_HQStatusReport* PendingStatus=nullptr;ERR_HQSearchReport* PendingSweep=nullptr;
ERR_HQAmmoReporting AmmoReporting;
float NextReport=0,NextStatus=0;bool fighting=false,eligible=true;
ERR_HQTask* Task=nullptr;void* Assignment=nullptr;void* Waypoint=nullptr;
bool Eligible(Mode*){return eligible;}
};
struct HQ{Mode mode;Mode* m_Mode=&mode;Network m_Network;Bodies m_Bodies;Journal m_Journal;
bool Fighting(ERR_HQUnit* u){return u->fighting;}
'''+methods+r'''
void Legacy(ERR_HQUnit* unit,float now){
'''+legacy+r'''
}
};
struct Stats{int sight=0,status=0,sweep=0;};
void Finish(ERR_HQUnit& u,Stats& s,bool failed=false){
assert(u.Uplink);if(u.PendingStatus){if(!failed)s.status++;}
else if(u.PendingSweep){if(!failed)s.sweep++;else u.Task->SweepQueued=false;}
else {assert(u.PendingReport);if(!failed)s.sight++;}
u.Uplink=nullptr;u.PendingStatus=nullptr;u.PendingSweep=nullptr;u.PendingReport=nullptr;
}
Stats Simulate(bool legacy){HQ h;ERR_HQUnit u;Stats counts;float due=-1;
// Model three native two-second legs; real transport is intentionally outside this harness.
for(int now=0;now<=600000;now+=1000){
if(u.Uplink&&now>=due)Finish(u,counts);
bool busy=u.Uplink;if(legacy)h.Legacy(&u,now);else h.QueueReports(&u,now);
if(!busy&&u.Uplink)due=now+6000;
}
assert(u.sampler.observations==601);return counts;
}
int main(){
auto before=Simulate(true);assert(before.sight==100&&before.status==0);
auto after=Simulate(false);assert(after.status>=4&&after.sight>=95);
HQ h;ERR_HQUnit u;Stats counts;
// Queueing creates no delivered roster knowledge; exact sampled time stays on the pending report.
h.QueueReports(&u,0);assert(u.PendingStatus&&u.PendingStatus->ObservedAt==0&&counts.status==0);
auto original=u.Uplink;h.QueueReports(&u,1000);assert(u.Uplink==original&&u.sampler.observations==2);
Finish(u,counts);assert(counts.status==1);h.QueueReports(&u,6000);assert(u.PendingReport);
// HQ check-in may be delivered while sighting uplink is busy; next free slot must carry status.
u.NextStatus=7000;h.QueueReports(&u,7000);assert(u.PendingReport&&!u.PendingStatus);
Finish(u,counts);h.QueueReports(&u,12000);assert(u.PendingStatus&&u.PendingStatus->ObservedAt==12000);
// Quiet-but-visible contact must suppress negative evidence, even though Fighting is false.
Finish(u,counts);ERR_HQTask task;task.ArrivedAt=0;u.Task=&task;u.Assignment=&task;u.Waypoint=&task;
h.QueueReports(&u,30000);assert(u.PendingReport&&!u.PendingSweep&&task.ArrivedAt==-1&&!u.fighting);
// Start dwell only once native perception sees no actual contact.
Finish(u,counts);u.ownUtility.visible=false;h.QueueReports(&u,36000);
assert(task.ArrivedAt==36000&&!u.Uplink);
// While a status frame is busy, newly visible contact still resets the dwell clock.
u.NextStatus=37000;h.QueueReports(&u,37000);assert(u.PendingStatus);
u.ownUtility.visible=true;h.QueueReports(&u,38000);
assert(task.ArrivedAt==-1&&u.PendingStatus&&!u.PendingReport);
// Clearing vision begins a fresh 30s dwell even while the existing uplink remains occupied.
u.ownUtility.visible=false;h.QueueReports(&u,39000);assert(task.ArrivedAt==39000);
Finish(u,counts);h.QueueReports(&u,43000);assert(!u.Uplink);
h.QueueReports(&u,68000);assert(!u.PendingSweep);
h.QueueReports(&u,69000);assert(u.PendingSweep&&u.PendingSweep->ObservedAt==69000&&task.SweepQueued);
// Delivered search is never queued a second time.
Finish(u,counts);h.QueueReports(&u,75000);assert(!u.Uplink&&counts.sweep==1);
// A failed negative retry remains subordinate to current positive observation.
task.SweepQueued=false;u.ownUtility.visible=true;h.QueueReports(&u,80000);
assert(u.PendingReport&&!u.PendingSweep&&task.ArrivedAt==-1);
// Quiet vision is insufficient while Fighting: combat and movement each reset uninterrupted dwell.
Finish(u,counts);u.ownUtility.visible=false;u.fighting=true;task.ArrivedAt=0;
h.QueueReports(&u,90000);assert(task.ArrivedAt==-1&&!u.PendingSweep);
u.fighting=false;h.QueueReports(&u,91000);assert(task.ArrivedAt==91000);
u.ownGroup.position.x=100;h.QueueReports(&u,92000);assert(task.ArrivedAt==-1&&!u.PendingSweep);
u.ownGroup.position.x=0;h.QueueReports(&u,93000);assert(task.ArrivedAt==93000);
h.QueueReports(&u,122000);assert(!u.PendingSweep);h.QueueReports(&u,123000);assert(u.PendingSweep);
// Route loss or an unavailable status sampler must not fabricate a report or block useful contacts.
HQ lost;ERR_HQUnit v;lost.m_Network.available=false;lost.QueueReports(&v,0);
assert(!v.Uplink&&v.NextStatus==0&&v.selection.attempts==0);
lost.m_Network.available=true;v.sampler.valid=false;lost.QueueReports(&v,6000);assert(v.PendingReport&&!v.PendingStatus);
// An unavailable radio cannot manufacture a frame or a delivery.
HQ quiet;ERR_HQUnit q;q.Radio=nullptr;quiet.QueueReports(&q,48000);assert(!q.Uplink);
// Body evidence still gets its physical radio frame ahead of a completed negative sweep.
ERR_HQTask qt;q.Radio=&q;q.NextStatus=999999;q.Task=&qt;q.Assignment=&qt;q.Waypoint=&qt;
qt.ArrivedAt=0;q.ownUtility.visible=false;quiet.m_Bodies.visible=true;
quiet.QueueReports(&q,50000);assert(q.PendingReport&&!q.PendingSweep);
}
''')
print('PASS: actual report methods reproduce old starvation; status-first scheduler delivers check-ins and requires 30s without combat or actual visible contacts before negative reports')