147 lines
10 KiB
Python
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')
|