Files
EscapistsReintroduced/tests/test_hq_uplink_fairness.py
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00

198 lines
15 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/core/Scripts/Game/ERR/10_AI/ERR_HQ.c')))
original=(ROOT/'addons/core/Scripts/Game/ERR/10_AI/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('ERR_HQLocalAwareness.Priority','ERR_HQLocalAwareness::Priority').replace('protected ','').replace('array<ref ERR_HQVisualContact>','Array<ERR_HQVisualContact*>')
for t in ('ERR_HQAmmoSnapshot','ERR_HQMountedAmmoSnapshot','ERR_HQUnit','ERR_HQVisualContact','ERR_HQRoute','ERR_HQReport','ERR_HQStatusReport','ERR_HQTask','ERR_HQSearchReport','IEntity'):
s=re.sub(r'\b'+t+r' (?=\w)',t+'* ',s)
s=s.replace('ERR_HQRoute* route;', 'ERR_HQRoute* route=nullptr;')
s=s.replace('ERR_HQReport* report;','ERR_HQReport* report = nullptr;')
for v in ('unit','route','report','seen','task','stock','mounted','actor'):
s=s.replace(v+'.',v+'->')
for v in ('Utility','ReportSelection','StatusSampler','Group','Task'):
s=s.replace('->'+v+'.','->'+v+'->')
s=s.replace('m_Mode.Settings().Runtime(', 'm_Mode->Settings()->Runtime(')
s=s.replace('ERR_HQPursuitRules.', 'ERR_HQPursuitRules::').replace('ERR_Actors.', 'ERR_Actors::').replace('CompartmentAccessComponent.', 'CompartmentAccessComponent::')
s=s.replace('ERR_HQObservation.','ERR_HQObservation::').replace('vector.DistanceXZ','vector::DistanceXZ').replace('string.Format','Format')
return s.replace('MountedAmmo.', 'MountedAmmo->').replace('AmmoState.', 'AmmoState->').replace('SecuritySite.IsEmpty()', 'SecuritySite.empty()')
policy_path=Path(os.environ.get('ERR_AMMO_REPORTING_ROOT', ROOT))/'addons/core/Scripts/Game/ERR/10_AI/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 bool SweepPresent(', 'protected void Sweep(', 'protected void QueueReports('))
run(r'''
#include <cassert>
#include <cmath>
#include <limits>
#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;int originReads=0;vector GetOrigin(){originReads++;return position;}};
struct IEntity{vector position;bool fit=true,attributed=true,aboard=false;int originReads=0;vector GetOrigin(){originReads++;return position;}};
struct ERR_Actors{static bool CanFight(IEntity* actor){return actor&&actor->fit;}};
struct CompartmentAccessComponent{static void* GetVehicleIn(IEntity* actor){return actor->aboard?actor:nullptr;}};
struct Settings{int interval=5;int Runtime(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);}static bool PositionValid(vector p){return Finite(p.x)&&Finite(p.z);}};
struct ERR_HQAmmoSnapshot{bool Complete=false,MineComplete=false;int ReadyMines=0,MineMembers=0,SmallArmsRounds=0,SmallArmsChambers=0,SmallArmsMuzzles=0,Members=0;bool Valid(int){return false;}};
struct ERR_HQMountedAmmoSnapshot{bool Complete=false;int Weapons=0,Muzzles=0,Magazines=0,Rounds=0,Capacity=0,Chambers=0,LowMagazines=0;
'''+body(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQMountedAmmoSampler.c',' bool Valid(')+r'''
};
struct ERR_HQStatusReport{ERR_HQMountedAmmoSnapshot* MountedAmmo=nullptr;ERR_HQAmmoSnapshot* AmmoState=nullptr;int MessageId=0,Count=4,Fit=4,ConfirmedLosses=0;string SourceId="own-1";float ObservedAt=0;};
'''+policy+r'''
struct Sampler{IEntity original;Group* group=nullptr;int strength=1;bool bound=true;IEntity* member=&original;
bool OwnsRoster(Group* requested){return bound&&group&&requested==group;}int InitialStrength(){return strength;}
IEntity* InitialMember(int slot){return slot==0?member:nullptr;}bool CanAttribute(int slot,IEntity* actor,bool observeLoss){assert(!observeLoss);return slot==0&&actor==&original&&actor->attributed;}
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{int OperationId=0;std::string SecuritySite;bool RoadIntercept=false,SweepQueued=false;float ArrivedAt=-1;vector Destination;
string WatchSourceId;int WatchMessageId=0,WatchRequestId=0,BeliefId=1,BeliefRevision=2;};
struct ERR_HQRoute{Group* LocalSource=nullptr;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 History{bool CanQueue(void*,float,float){return true;}void Queued(void*,float,float){}};
struct ERR_HQLocalAwareness{static bool Priority(float,float,float){return false;}};
struct ERR_HQUnit{bool ExecutionTurn=true;float NextContact=0;History SentObservations;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;
ERR_HQUnit(){sampler.group=&ownGroup;}
bool Eligible(Mode*){return eligible;}
};
struct HQ{bool m_Coordinated=false;void QueueExecution(ERR_HQUnit*,float){}void ContactReport(ERR_HQUnit*,float){}
Mode mode;Mode* m_Mode=&mode;Network m_Network;Bodies m_Bodies;Journal m_Journal;
bool OriginOccupied(void*,ERR_HQUnit*){return false;}
ERR_HQRoute* ObservationRoute(ERR_HQUnit* u){return u->Radio?m_Network.Route(u->Radio):nullptr;}
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(){
// Only original physical presence can accumulate negative-search dwell.
// A group left at the spawn anchor cannot hide arrival or fabricate it.
{HQ h;ERR_HQUnit u;ERR_HQTask task;u.Task=&task;u.Assignment=&task;u.Waypoint=&task;u.ownUtility.visible=false;
u.ownGroup.position={1000,1000};u.sampler.original.position={40,0};
h.Sweep(&u,1000);assert(task.ArrivedAt==1000&&!u.Uplink&&u.ownGroup.originReads==0);
h.Sweep(&u,30999);assert(!u.PendingSweep);h.Sweep(&u,31000);
assert(u.PendingSweep&&u.PendingSweep->ObservedAt==31000&&u.PendingSweep->Position.x==task.Destination.x);
auto report=u.PendingSweep;u.sampler.original.position={900,900};h.Sweep(&u,32000);
assert(u.PendingSweep==report&&report->ObservedAt==31000&&report->Position.x==0);}
for(int invalid=0;invalid<10;invalid++){
HQ h;ERR_HQUnit u;ERR_HQTask task;task.ArrivedAt=0;u.Task=&task;u.Assignment=&task;u.Waypoint=&task;u.ownUtility.visible=false;
IEntity replacement;Group foreign;
if(invalid==0){u.sampler.original.position={40.01f,0};}
if(invalid==1){u.sampler.original.fit=false;}
if(invalid==2){u.sampler.original.attributed=false;}
if(invalid==3){u.sampler.original.aboard=true;}
if(invalid==4){u.sampler.member=&replacement;}
if(invalid==5){u.sampler.bound=false;}
if(invalid==6){u.sampler.group=&foreign;}
if(invalid==7){u.sampler.strength=33;}
if(invalid==8){u.sampler.original.position.x=std::numeric_limits<float>::quiet_NaN();}
if(invalid==9){u.eligible=false;}
h.Sweep(&u,31000);assert(task.ArrivedAt==-1&&!u.PendingSweep&&!u.Uplink&&u.ownGroup.originReads==0);
if(invalid>0&&invalid<8){assert(u.sampler.original.originReads==0&&replacement.originReads==0);}
}
// An eligibility interruption restarts the full dwell instead of preserving credit.
{HQ h;ERR_HQUnit u;ERR_HQTask task;u.Task=&task;u.Assignment=&task;u.Waypoint=&task;u.ownUtility.visible=false;
h.Sweep(&u,1000);u.eligible=false;h.Sweep(&u,20000);assert(task.ArrivedAt==-1);
u.eligible=true;h.Sweep(&u,31000);assert(task.ArrivedAt==31000&&!u.PendingSweep);
h.Sweep(&u,60999);assert(!u.PendingSweep);h.Sweep(&u,61000);assert(u.PendingSweep&&u.PendingSweep->ObservedAt==61000);}
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.sampler.original.position.x=100;h.QueueReports(&u,92000);assert(task.ArrivedAt==-1&&!u.PendingSweep);
u.sampler.original.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);
// A staffed road hold must never manufacture negative search evidence from idle dwell.
HQ road;ERR_HQUnit held;ERR_HQTask hold;hold.RoadIntercept=true;hold.ArrivedAt=0;
held.Task=&hold;held.Assignment=&hold;held.Waypoint=&hold;held.ownUtility.visible=false;
road.Sweep(&held,60000);assert(!held.PendingSweep&&!held.Uplink&&!hold.SweepQueued);
}
''')
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')