Capture current mission, AI, map and administration development

Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
This commit is contained in:
2026-10-13 22:52:53 +02:00
parent a951a96edc
commit 6a667d8deb
8104 changed files with 2124945 additions and 2061 deletions
@@ -0,0 +1,240 @@
"""Actual radio task delivery/ownership methods with native waypoint and memory doubles."""
from pathlib import Path
import os,re
from policy_harness import ROOT,body,run
P=Path(os.environ.get('ERR_HQ_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'))
ROAD=Path(os.environ.get('ERR_HQ_ROAD_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQRoadIntercept.c'))
def cpp(s):
s=s.replace('EngineerMine.', 'EngineerMine->')
s=s.replace('ref ', '')
s=s.replace('protected ','').replace('null','nullptr').replace('string.Format','Format')
for name in ('Math','ERR_GameMode','SCR_EntityHelper','ERR_HQPursuitRules','AIWaypoint','ERR_Actors','ERR_Resources','ERR_ForceDuty','ERR_HQRoadIntercept','ERR_ForceRules','ERR_HQPursuitRole','ERR_HQMemoryKind'):
s=s.replace(name+'.',name+'::')
for typ in ('ERR_HQUnit','ERR_HQTask','ERR_GameMode','ERR_HQReport','ERR_HQBelief','ERR_ForceRecord','ERR_HQHuntWindow','AIWaypoint'):
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
s=re.sub(r'foreach \((ERR_HQUnit\*) (\w+) : ([^)]+)\)',r'for (\1 \2 : \3)',s)
for name in ('unit','other','task','planned','order','belief','record','mode','Group','hunt','waypoint','m_Mode','lostActive','lostPending'):
s=s.replace(name+'.',name+'->')
s=s.replace('ResourceName ', 'int ').replace('.IsEmpty()', '.empty()')
s=s.replace('PendingOrder.', 'PendingOrder->').replace('Assignment.', 'Assignment->')
s=s.replace('PendingTask.', 'PendingTask->').replace('Task.', 'Task->')
s=s.replace('Downlink.', 'Downlink->').replace('Forces().','Forces()->').replace('ERR_ForceRecord* record;','ERR_ForceRecord* record=nullptr;')
return s
units='\n'.join(cpp(body(P,x)) for x in ('void CancelPending(', 'ERR_HQTask PlannedTask(', 'void ReleaseAssignment(', 'void Release()'))
methods='\n'.join(cpp(body(P,x)) for x in ('protected bool AlarmCommitted(', 'protected void FinishWatch(', 'protected void ReleaseUnit(', 'protected void LostOrder(', 'protected void DeliverOrder('))
if 'protected float HuntCommitment(' in P.read_text(): methods=cpp(body(P,'protected float HuntCommitment('))+'\n'+methods
if 'protected void ReconsiderHunt(' in P.read_text(): methods=cpp(body(P,'protected void ReconsiderHunt('))+'\n'+methods
if 'protected void RememberCompletedHunt(' in P.read_text(): methods=cpp(body(P,'protected void RememberCompletedHunt('))+'\n'+methods
if 'protected void ReleaseLostForce(' in P.read_text(): methods=cpp(body(P,'protected void ReleaseLostForce('))+'\n'+methods
missing=cpp(body(P,'if (!unit.Group && !unit.Uplink)')).replace('m_Units.Remove(i)', 'm_Units.erase(m_Units.begin()+i)')
cleanup=cpp(body(P,' foreach (ERR_HQUnit unit : m_Units)\n {\n if (!unit.Assignment && !unit.PendingOrder) continue;\n if (!unit.PlanEligible || assigned > globalLimit)'))
R=Path(os.environ.get('ERR_HQ_PURSUIT_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQPursuit.c'))
rules='\n'.join(cpp(body(R,x)) for x in ('static bool Finite(', 'static bool PositionValid(', 'static bool Actionable(', 'static bool Heading(', 'static ERR_HQPursuitRole Role('))
eligible=cpp(body(ROAD,'static bool Eligible('))
run(r'''
#include <cassert>
#include <vector>
#include <string>
#include <algorithm>
#include <cmath>
#include <limits>
using string=std::string;
template<class... T>string Format(T...){return "order";}
struct vector{float x=0,y=0,z=0;float& operator[](int i){return i==0?x:(i==1?y:z);}float Length(){return std::sqrt(x*x+y*y+z*z);}};
enum class ERR_HQPursuitRole{SEARCH,INTERCEPT,CONTAIN};
struct ERR_ForceRules{static bool Finite(float f){return std::isfinite(f);}};
struct ERR_HQMemoryKind{static const int SIGHTING=1,ALARM=4;};
struct ERR_HQPursuitRules{
'''+rules+r'''
};
struct Math{static float Min(float a,float b){return std::min(a,b);}static float Max(float a,float b){return std::max(a,b);}};
struct ERR_HQTask{string SecuritySite;bool RoadIntercept=false,ReleaseOnly=false;ERR_HQTask* ReplacesTask=nullptr;string WatchSourceId;int WatchMessageId=0,WatchRequestId=0;
int BeliefId=1,BeliefRevision=2;ERR_HQPursuitRole Role=ERR_HQPursuitRole::SEARCH;vector Destination{40,0,50};float SearchRadiusMetres=60,QueuedAt=0,ExpiresAt=60000;bool Investigation(){return !WatchSourceId.empty();}string Name(){return "search";}};
struct ERR_HQReport{string TravelMode="on foot";int MessageId=10;float ObservedAt=0;};
struct ERR_HQRoute{int MessageId=20;};
struct AIWaypoint{int resource=0,deletes=0,configured=0;float radius=30;void SetCompletionRadius(float r){assert(!configured);radius=r;configured++;}static AIWaypoint* Cast(AIWaypoint* p){return p;}};
struct SCR_AIGroup{int removals=0,additions=0;AIWaypoint* last=nullptr;void RemoveWaypoint(AIWaypoint* w){removals++;last=w;}void AddWaypointAt(AIWaypoint* w,int at){assert(at==0);additions++;last=w;}};
struct ERR_ForceRecord{bool Eliminated=false;int Duty=2,HomeDuty=1;vector DutyTarget,HomePosition;};
struct Ledger{ERR_ForceRecord record;ERR_ForceRecord* FindGroup(SCR_AIGroup*){return &record;}};
struct ERR_GameMode{static inline ERR_GameMode* current=nullptr;Ledger ledger;static ERR_GameMode* Get(){return current;}Ledger* Forces(){return &ledger;}};
struct SCR_EntityHelper{static void DeleteEntityAndChildren(AIWaypoint* p){p->deletes++;}};
struct ERR_Actors{static inline bool refuse=false;static inline int spawns=0;static AIWaypoint* Spawn(int resource,vector){spawns++;if(refuse)return nullptr;auto w=new AIWaypoint;w->resource=resource;return w;}static vector Ground(vector p){return p;}};
struct ERR_Resources{static int SearchWaypoint(){return 1;}static int DefendWaypoint(){return 2;}};
struct ERR_ForceDuty{static const int SEARCH=2;};
struct MineJob {int stops=0;void Stop(){stops++;}};
struct ERR_HQUnit{MineJob* EngineerMine=nullptr;int Id=1;string ForceSourceId="original";SCR_AIGroup* Group=nullptr;AIWaypoint* Waypoint=nullptr;
ERR_HQTask* Task=nullptr;ERR_HQTask* PendingTask=nullptr;ERR_HQReport* Assignment=nullptr;ERR_HQReport* PendingOrder=nullptr;
ERR_HQRoute* Downlink=nullptr;ERR_HQRoute* Uplink=nullptr;void* PendingConvoyOrder=nullptr;bool HomeGuard=false,PendingCheckin=false,eligible=true,fighting=false,PlanEligible=true;
bool Eligible(ERR_GameMode*){return eligible&&Group;}
'''+units+r'''
};
struct ERR_HQBelief{int BeliefId=1,BeliefRevision=2,Kind=1,SampleCount=2,MessageId=10;float Confidence=.8f,Radius=60,ObservedAt=0,LastDeliveredAt=0;string TravelMode="on foot";vector Velocity{0,0,3};bool HasVelocity=true;};
struct ERR_HQRoadIntercept{
'''+eligible+r'''
};
struct Memory{ERR_HQBelief* belief=nullptr;ERR_HQBelief* GetById(int id){return belief&&belief->BeliefId==id?belief:nullptr;}};
struct Watch{bool empty=false;int finished=0;bool ArmedSearchBlocked(string,float){return empty;}bool FinishInvestigation(string,int,int,float){finished++;return true;}};
struct Journal{int writes=0;void Write(string,string,int=0){writes++;}};
struct ERR_HQHuntWindow{float CompletedUntil=0;float NextHunt=200000;};
struct HQ{ERR_GameMode* m_Mode;Memory m_Memory;Watch m_Watch;Journal m_Journal;std::vector<ERR_HQUnit*>m_Units;ERR_HQHuntWindow hunt,otherHunt;bool watchValid=true;
bool Fighting(ERR_HQUnit* u){return u->fighting;}bool WatchTaskValid(ERR_HQTask*){return watchValid;}ERR_HQHuntWindow* Hunt(int id){return id==2?&otherHunt:&hunt;}
'''+methods+'\nvoid Cleanup(float now,int assigned,int globalLimit){'+cleanup+'}\nvoid RemoveMissing(float now){for(int i=int(m_Units.size())-1;i>=0;i--){ERR_HQUnit* unit=m_Units[i];'+missing+'}}'+r'''
};
struct Fixture{ERR_GameMode mode;HQ hq;ERR_HQUnit unit;SCR_AIGroup group;AIWaypoint previous;ERR_HQTask active,next;ERR_HQReport oldReport,newReport;ERR_HQRoute route;ERR_HQBelief belief;
Fixture(){ERR_GameMode::current=&mode;ERR_Actors::refuse=false;ERR_Actors::spawns=0;hq.m_Mode=&mode;hq.m_Units={&unit};hq.m_Memory.belief=&belief;
unit.Group=&group;unit.Task=&active;unit.Assignment=&oldReport;unit.Waypoint=&previous;unit.PendingTask=&next;unit.PendingOrder=&newReport;unit.Downlink=&route;active.BeliefId=9;}
void Retained(){assert(unit.Task==&active&&unit.Assignment==&oldReport&&unit.Waypoint==&previous&&previous.deletes==0&&group.removals==0&&!unit.PendingTask&&!unit.PendingOrder&&!unit.Downlink);}
};
int main(){
// The copied radius is used before installation even if current uncertainty grows.
for(float radius:{45.f,60.f,110.f}){Fixture f;f.next.SearchRadiusMetres=radius;f.belief.Radius=500;
float deadline=f.next.ExpiresAt;f.hq.DeliverOrder(&f.unit,1000);
assert(f.group.additions==1&&f.unit.Waypoint->radius==radius&&f.unit.Waypoint->configured==1);
assert(f.next.SearchRadiusMetres==radius&&f.next.ExpiresAt==deadline);}
// Malformed snapshot cannot spawn or replace the delivered previous assignment.
for(float radius:{0.f,44.99f,110.01f,-1.f,std::numeric_limits<float>::quiet_NaN(),std::numeric_limits<float>::infinity()}){
Fixture f;f.next.SearchRadiusMetres=radius;f.hq.DeliverOrder(&f.unit,1000);f.Retained();assert(ERR_Actors::spawns==0);}
// Missing patrol, road/security defend, interception and containment keep native defaults.
for(int kind=0;kind<5;kind++){Fixture f;f.next.SearchRadiusMetres=std::numeric_limits<float>::quiet_NaN();
if(kind==0)f.next.WatchSourceId="missing-original";
if(kind==1)f.next.RoadIntercept=true;
if(kind==2)f.next.SecuritySite="site";
if(kind==3)f.next.Role=ERR_HQPursuitRole::INTERCEPT;
if(kind==4){f.next.Role=ERR_HQPursuitRole::CONTAIN;f.belief.Confidence=.9f;}
f.hq.DeliverOrder(&f.unit,1000);assert(f.group.additions==1&&f.unit.Waypoint->radius==30&&f.unit.Waypoint->configured==0);}
// Alarm-only deliveries retain their weak SEARCH/mobile policy even if a queued task is tampered with.
for(int invalid=0;invalid<5;invalid++){Fixture f;f.belief.Kind=ERR_HQMemoryKind::ALARM;
ERR_HQUnit other;ERR_HQTask otherTask;ERR_HQReport alarm;alarm.TravelMode="heard vehicle alarm";
if(invalid==0)f.unit.HomeGuard=true;
if(invalid==1)f.next.Role=ERR_HQPursuitRole::INTERCEPT;
if(invalid==2)f.next.RoadIntercept=true;
if(invalid==3){other.PendingTask=&otherTask;other.PendingOrder=&alarm;f.hq.m_Units.push_back(&other);}
if(invalid==4){other.Task=&otherTask;other.Assignment=&alarm;f.hq.m_Units.push_back(&other);}
f.hq.DeliverOrder(&f.unit,1000);f.Retained();assert(ERR_Actors::spawns==0);}
{Fixture f;f.belief.Kind=ERR_HQMemoryKind::ALARM;f.newReport.TravelMode="heard vehicle alarm";
assert(f.hq.AlarmCommitted()&&!f.hq.AlarmCommitted(&f.unit));
f.hq.DeliverOrder(&f.unit,1000);assert(ERR_Actors::spawns==1&&f.unit.Waypoint->resource==1&&f.hq.AlarmCommitted());
f.unit.ReleaseAssignment();assert(!f.hq.AlarmCommitted());}
// A changed belief revision or absent/stale/uncertain track cannot create a native destination.
for(int invalid=0;invalid<5;invalid++){Fixture f;
if(invalid==0){f.belief.BeliefRevision=3;}if(invalid==1){f.hq.m_Memory.belief=nullptr;}
if(invalid==2){f.belief.ObservedAt=2000;}if(invalid==3){f.belief.Confidence=.1f;}if(invalid==4){f.belief.Radius=2001;}
f.hq.DeliverOrder(&f.unit,1000);f.Retained();assert(ERR_Actors::spawns==0);}
// Exact absolute expiry boundary, combat, physical reservation eligibility and known-empty ammo remain gates.
for(int invalid=0;invalid<4;invalid++){Fixture f;
if(invalid==0){f.next.ExpiresAt=1000;}if(invalid==1){f.unit.fighting=true;}if(invalid==2){f.unit.eligible=false;}if(invalid==3){f.hq.m_Watch.empty=true;}
f.hq.DeliverOrder(&f.unit,1000);f.Retained();assert(ERR_Actors::spawns==0);}
// Native spawn refusal must leave the delivered old waypoint and duty intact.
{Fixture f;ERR_Actors::refuse=true;f.hq.DeliverOrder(&f.unit,1000);f.Retained();assert(ERR_Actors::spawns==1&&f.mode.ledger.record.Duty==2);}
// Commit swaps the old task only after native success. Duplicate delivery has no pending lineage and cannot spawn twice.
{Fixture f;f.hq.DeliverOrder(&f.unit,1000);assert(ERR_Actors::spawns==1&&f.previous.deletes==1&&f.group.removals==1&&f.group.additions==1);
assert(f.unit.Waypoint->resource==1);
assert(f.unit.Task==&f.next&&f.unit.Assignment==&f.newReport&&f.unit.Waypoint!=&f.previous&&!f.unit.PendingTask&&!f.unit.Downlink);
auto retained=f.unit.Waypoint;f.hq.DeliverOrder(&f.unit,1001);assert(ERR_Actors::spawns==1&&f.unit.Waypoint==retained&&f.group.additions==1);
assert(f.mode.ledger.record.DutyTarget.x==f.next.Destination.x);f.unit.ReleaseAssignment();assert(retained->deletes==1&&f.mode.ledger.record.Duty==f.mode.ledger.record.HomeDuty);}
// Actual assignment-pass expiry releases only the expired reservation, preserving the other lineage.
{Fixture f;f.active.ExpiresAt=1000;f.hq.Cleanup(1000,1,4);
assert(!f.unit.Task&&!f.unit.Assignment&&!f.unit.Waypoint&&f.previous.deletes==1&&f.unit.PendingTask==&f.next&&f.unit.Downlink==&f.route);
assert(f.mode.ledger.record.Duty==f.mode.ledger.record.HomeDuty);f.hq.Cleanup(1001,1,4);assert(f.previous.deletes==1&&f.unit.PendingTask==&f.next);}
{Fixture f;f.active.BeliefId=1;f.next.ExpiresAt=1000;f.hq.Cleanup(1000,1,4);f.Retained();}
{Fixture f;f.unit.PlanEligible=false;f.hq.Cleanup(1000,1,4);assert(!f.unit.Task&&!f.unit.PendingTask&&!f.unit.Downlink&&f.previous.deletes==1);}
// Loss of the only physical pursuit squad releases its unfulfilled long hold.
{Fixture f;f.unit.CancelPending();f.active.BeliefId=1;f.active.ExpiresAt=600000;f.hq.hunt.NextHunt=780000;f.unit.PlanEligible=false;
f.hq.Cleanup(10000,1,4);assert(f.hq.hunt.NextHunt==25000);assert(!f.unit.Task&&!f.unit.Assignment&&f.previous.deletes==1);}
// Native group deletion takes the earlier Tick cleanup path, before Assign.
{Fixture f;f.unit.CancelPending();f.active.BeliefId=1;f.active.ExpiresAt=600000;f.hq.hunt.NextHunt=780000;f.unit.Group=nullptr;
f.hq.RemoveMissing(10000);assert(f.hq.m_Units.empty()&&f.hq.hunt.NextHunt==25000&&!f.unit.Task&&f.previous.deletes==1);}
// Finishing an ordinary delivered search retains its completed-hunt cooldown.
{Fixture f;f.unit.CancelPending();f.active.BeliefId=1;f.active.ExpiresAt=10000;f.hq.hunt.NextHunt=190000;
f.hq.Cleanup(10000,1,4);assert(f.hq.hunt.NextHunt==190000&&!f.unit.Assignment);}
// Simultaneous expiry and unavailable-force detection cannot erase a completed
// hunt's quiet interval, including after loss of a pending same-belief retask.
for(int removed=0;removed<2;removed++)for(int pending=0;pending<3;pending++)for(int shorter=0;shorter<2;shorter++){
Fixture f;f.active.BeliefId=1;f.active.ExpiresAt=10000;f.hq.hunt.NextHunt=shorter?12000:190000;f.hq.otherHunt.NextHunt=780000;
if(pending==0)f.unit.CancelPending();else {f.next.BeliefId=pending==1?1:2;f.next.ExpiresAt=600000;}
f.unit.PlanEligible=false;if(removed){f.unit.Group=nullptr;f.hq.RemoveMissing(10000);}else f.hq.Cleanup(10000,1,4);
assert(f.hq.hunt.NextHunt==(shorter?12000:190000));assert(f.hq.otherHunt.NextHunt==(pending==2?25000:780000));
assert(!f.unit.Task&&!f.unit.PendingTask&&f.previous.deletes==1);
}
// A completed squad's floor survives a different lost squad in either cleanup
// order, whether the completed squad expires normally or is also unavailable.
for(int reverse=0;reverse<2;reverse++)for(int completedLost=0;completedLost<2;completedLost++){
Fixture f;f.unit.CancelPending();f.active.BeliefId=1;f.active.ExpiresAt=10000;f.unit.PlanEligible=!completedLost;f.hq.hunt.NextHunt=780000;
ERR_HQUnit lost;SCR_AIGroup lostGroup;ERR_HQTask lostTask;ERR_HQReport lostReport;lost.Group=&lostGroup;lost.Task=&lostTask;lost.Assignment=&lostReport;lost.PlanEligible=false;lostTask.BeliefId=1;lostTask.ExpiresAt=600000;
f.hq.m_Units.push_back(&lost);if(reverse)std::reverse(f.hq.m_Units.begin(),f.hq.m_Units.end());f.hq.Cleanup(10000,2,4);assert(f.hq.hunt.NextHunt==190000);
}
// A replacement can arrive before Assign processes the original expiry.
// Native commit must preserve completion history before clearing old ownership.
{Fixture f;f.active.BeliefId=1;f.active.ExpiresAt=10000;f.next.ExpiresAt=600000;f.hq.hunt.NextHunt=780000;
f.hq.DeliverOrder(&f.unit,10000);assert(f.unit.Task==&f.next&&f.hq.hunt.CompletedUntil==190000);
f.unit.PlanEligible=false;f.hq.Cleanup(11000,1,4);assert(f.hq.hunt.NextHunt==190000&&!f.unit.Task);}
// Only expired delivered pursuit work creates completion history.
for(int kind=0;kind<5;kind++){Fixture f;f.active.BeliefId=1;f.active.ExpiresAt=10000;
if(kind==0){f.active.ExpiresAt=10001;}if(kind==1){f.active.ReleaseOnly=true;}if(kind==2){f.active.SecuritySite="security";}if(kind==3){f.active.WatchSourceId="watch";}if(kind==4){f.unit.Assignment=nullptr;}
f.hq.ReleaseUnit(&f.unit,10000);assert(f.hq.hunt.CompletedUntil==0&&f.hq.otherHunt.CompletedUntil==0);}
// Capacity trimming is deliberately unchanged: it is not evidence of force loss.
{Fixture f;f.unit.CancelPending();f.active.BeliefId=1;f.active.ExpiresAt=600000;f.hq.hunt.NextHunt=780000;
f.hq.Cleanup(10000,2,1);assert(f.hq.hunt.NextHunt==780000&&!f.unit.Assignment);}
// Surviving active and pending same-belief commitments both retain their own
// horizon; foreign replacements cannot hide active work or prolong this hunt.
for(int scenario=0;scenario<5;scenario++){Fixture f;f.unit.CancelPending();f.active.BeliefId=1;f.active.ExpiresAt=600000;f.hq.hunt.NextHunt=780000;f.unit.PlanEligible=false;
ERR_HQUnit peer;SCR_AIGroup peerGroup;ERR_HQTask surviving,replacement;ERR_HQReport activeReport,pendingReport;peer.Group=&peerGroup;peer.Task=&surviving;peer.Assignment=&activeReport;peer.PendingTask=&replacement;peer.PendingOrder=&pendingReport;
surviving.BeliefId=1;surviving.ExpiresAt=120000;replacement.BeliefId=1;replacement.ExpiresAt=180000;
if(scenario==1)replacement.BeliefId=2;
if(scenario==2){surviving.SecuritySite="outpost";replacement.ReleaseOnly=true;}
if(scenario==3){peer.Assignment=nullptr;peer.PendingOrder=nullptr;}
if(scenario==4)f.hq.hunt.NextHunt=12000;
f.hq.m_Units.push_back(&peer);f.hq.Cleanup(10000,2,4);float expected=scenario==0?360000:scenario==1?300000:scenario==4?12000:25000;
assert(f.hq.hunt.NextHunt==expected);assert(peer.Task==&surviving);}
// Expired undelivered retasks use the same bounded rollback as lost radio.
// Both original and replacement expire here; no unfulfilled hunt owns 780s.
{Fixture f;f.active.ExpiresAt=120000;f.next.ExpiresAt=600000;f.hq.hunt.NextHunt=780000;
f.hq.Cleanup(600000,1,4);assert(f.hq.hunt.NextHunt==615000);
assert(!f.unit.Task&&!f.unit.PendingTask&&!f.unit.Downlink&&f.previous.deletes==1);}
// A still-live containment order also needs current authorization at delivery.
// The fixed task deadline is not permission to use an older/weaker movement belief.
for(int invalid=0;invalid<6;invalid++){Fixture f;f.next.Role=ERR_HQPursuitRole::CONTAIN;
f.next.QueuedAt=30000;f.next.ExpiresAt=120000;float now=45000;
if(invalid==0){now=45001;}
if(invalid==1){f.belief.Radius=100.01f;}
if(invalid==2){f.belief.Confidence=.799f;}
if(invalid==3){f.belief.HasVelocity=false;}
if(invalid==4){f.belief.Velocity={0,0,.49f};}
if(invalid==5){f.belief.Velocity={0,0,71};}
f.hq.DeliverOrder(&f.unit,now);f.Retained();assert(ERR_Actors::spawns==0);}
{Fixture f;f.next.Role=ERR_HQPursuitRole::CONTAIN;f.next.QueuedAt=30000;f.next.ExpiresAt=120000;
f.belief.Radius=100;f.belief.Confidence=.8f;f.belief.Velocity={0,0,.5f};
f.hq.DeliverOrder(&f.unit,45000);assert(ERR_Actors::spawns==1&&f.unit.Task==&f.next&&f.previous.deletes==1);
f.hq.DeliverOrder(&f.unit,45001);assert(ERR_Actors::spawns==1&&f.group.additions==1);}
// Radio latency may age a precision interception beyond its 45-second authorization.
// Eligibility uses the actual road helper and pursuit rules, never a test predicate.
for(int invalid=0;invalid<9;invalid++){Fixture f;f.next.RoadIntercept=true;float now=1000;
if(invalid==0)now=45001;
if(invalid==1)f.belief.LastDeliveredAt=1001;
if(invalid==2)f.belief.SampleCount=1;
if(invalid==3)f.belief.Velocity={0,0,1.99f};
if(invalid==4)f.belief.HasVelocity=false;
if(invalid==5)f.belief.TravelMode="aircraft";
if(invalid==6)f.belief.Kind=2;
if(invalid==7)f.belief.MessageId=0;
if(invalid==8){f.belief.ObservedAt=1;f.belief.LastDeliveredAt=0;}
f.hq.DeliverOrder(&f.unit,now);f.Retained();assert(ERR_Actors::spawns==0);}
{Fixture f;f.next.RoadIntercept=true;f.hq.DeliverOrder(&f.unit,45000);
assert(ERR_Actors::spawns==1&&f.unit.Waypoint&&f.unit.Waypoint->resource==2&&f.unit.Task==&f.next&&f.previous.deletes==1);
f.hq.DeliverOrder(&f.unit,45001);assert(ERR_Actors::spawns==1&&f.group.additions==1);
f.hq.Cleanup(60000,1,4);assert(!f.unit.Task&&!f.unit.Waypoint&&f.mode.ledger.record.Duty==f.mode.ledger.record.HomeDuty);}
// Security orders carry an inspected-site hypothesis, not a fabricated enemy track.
{Fixture f;f.next.SecuritySite="mortar-1";f.next.BeliefId=999;
f.hq.DeliverOrder(&f.unit,1000);assert(f.unit.Task==&f.next&&f.unit.Waypoint->resource==2);
f.hq.Cleanup(1001,1,4);assert(f.unit.Task==&f.next);
f.hq.Cleanup(60000,1,4);assert(!f.unit.Task&&!f.unit.Waypoint);}
// Cancelling a mismatched recall never releases a replacement task which arrived first.
{Fixture f;f.next.ReleaseOnly=true;f.next.ReplacesTask=nullptr;f.hq.DeliverOrder(&f.unit,1000);f.Retained();assert(ERR_Actors::spawns==0);}
{Fixture f;f.next.ReleaseOnly=true;f.next.ReplacesTask=&f.active;f.hq.DeliverOrder(&f.unit,1000);assert(!f.unit.Task&&!f.unit.Assignment&&!f.unit.Waypoint&&f.previous.deletes==1&&ERR_Actors::spawns==0);}
}
''')
print('PASS: actual delivered-task lineage, expiry, old-order retention, native commit, release, road precision expiry and duplicate delivery')
delivery=body(P,'protected void DeliverOrder(')
assert delivery.index('waypoint.SetCompletionRadius(task.SearchRadiusMetres)') < delivery.index('unit.Release();') < delivery.index('unit.Group.AddWaypointAt')
@@ -0,0 +1,105 @@
"""Actual planner selection/commit body: identical orders with one local count scan."""
import os,re
from pathlib import Path
from policy_harness import ROOT,body,run
P=Path(os.environ.get('ERR_HQ_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'))
s=body(P,'protected void Assign(')
# Execute the complete final selection/commit loop. The preceding cleanup,
# Retask and Investigate boundaries are represented by pre-existing commitments.
start=s.index(' assigned = HuntingAssigned();')
code=s[start:s.rfind('}')]
# In the reference variant, restore only the old repeated-count loop guards.
legacy=code.replace(' int totalAssigned = Assigned();','').replace('totalAssigned < globalLimit','Assigned() < globalLimit').replace(' totalAssigned++;','')
assigned=body(P,'protected int Assigned(').replace('protected ','').replace('int Assigned()', 'int ActualAssigned()')
def adapt(s):
s=s.replace('array<vector>','Arr<Vec>').replace('array<int>','Arr<int>').replace('vector destination','Vec destination').replace('vector position','Vec position')
s=s.replace('foreach (ERR_HQUnit unit : m_Units)','for (ERR_HQUnit* unit : m_Units)')
for cls in ('ERR_HQBelief','ERR_HQHuntWindow','ERR_HQUnit','ERR_HQRoute','ERR_HQTask'):
s=re.sub(r'\b'+cls+r' (\w+)',cls+r'* \1',s)
s=re.sub(r'(ERR_HQ(?:Belief|Unit|Route)\* \w+);',r'\1=nullptr;',s)
s=s.replace('int total;', 'int total=0;')
s=s.replace('bool roadIntercept;', 'bool roadIntercept=false;').replace('int bestId;', 'int bestId=0;')
for name in ('unit','belief','candidate','hunt','best','bestRoute','task'):
s=s.replace(name+'.',name+'->')
s=s.replace('unit->Group.GetOrigin()', 'unit->Group->GetOrigin()').replace('belief->Report.Snapshot()', 'belief->Report->Snapshot()')
s=s.replace('new ERR_HQTask()', 'NewTask()').replace('null','nullptr').replace('nullptrptr','nullptr')
for name in ('ERR_HQPursuitRules','ERR_HQPursuitRole','ERR_HQMemoryKind','ERR_HQRoadIntercept','Math'):
s=s.replace(name+'.',name+'::')
s=s.replace('vector.DistanceXZ','Distance').replace('string.Format','Format').replace('ERR_HQPursuitRole role','int role')
return s
run(r'''
#include <cassert>
#include <string>
#include <vector>
#include <map>
#include <algorithm>
#include <cmath>
#include <memory>
using string=std::string;
template<class T> struct Arr:std::vector<T>{using std::vector<T>::vector;int Count()const{return this->size();}void Insert(T t){this->push_back(t);}bool Contains(T t){return std::find(this->begin(),this->end(),t)!=this->end();}};
struct Vec{float x=0;bool operator==(Vec b)const{return x==b.x;}};
float Distance(Vec a,Vec b){return std::abs(a.x-b.x);}
struct Math{static float Max(float a,float b){return std::max(a,b);}};
template<class...T>string Format(string s,T...){return s;}
namespace ERR_HQMemoryKind{enum{SIGHTING,GUNFIRE,ALARM};}
namespace ERR_HQPursuitRole{enum{SEARCH,INTERCEPT,CONTAIN};}
struct ERR_HQReport{int id=0;ERR_HQReport* Snapshot(){return this;}};
struct ERR_HQTask{int BeliefId=0,BeliefRevision=0,Role=0,ApproachBearing=0;bool RoadIntercept=false;Vec Destination;float SearchRadiusMetres=0,QueuedAt=0,ExpiresAt=0;string Name(){return "task";}};
struct ERR_HQRoute{int MessageId=0;string Payload;};
struct Group{Vec pos;Vec GetOrigin(){return pos;}};
struct ERR_HQUnit{int Id=0,Radio=0,HomeId=0,ForceSourceId=0;struct Group* Group=nullptr;ERR_HQReport* Assignment=nullptr;ERR_HQReport* PendingOrder=nullptr;ERR_HQTask* PendingTask=nullptr;ERR_HQRoute* Downlink=nullptr;bool PlanFighting=false,PlanEligible=true,HomeGuard=false;Vec PlanPosition;};
struct ERR_HQBelief{int BeliefId=0,BeliefRevision=1,MessageId=1,Kind=0;float Confidence=.7,ObservedAt=1000,Radius=100;Vec Position,PredictedPosition,Velocity;bool HasVelocity=true;ERR_HQReport* Report=nullptr;};
struct ERR_HQHuntWindow{float NextHunt=0;};
struct ERR_HQPursuitRules{
static float SearchRadius(float radius){return std::clamp(radius,45.f,110.f);}
static bool Actionable(float c,float age,float r){return c>=.15&&age>=0&&r<=2000;}
static bool Heading(Vec,bool h){return h;}
static int Role(float,float,float,bool h,int slot){return h?slot:0;}
static Vec Objective(Vec p,Vec,bool,float,float,float,int role,int slot){return {p.x+role*10+slot};}
static bool CanCommit(bool group,bool eligible,bool busy,bool guard,int available,int assigned,int limit){return group&&eligible&&!busy&&(!guard||available>1)&&assigned<limit;}
static int Bearing(Vec p,Vec){return ((int)p.x)%2;}
static bool BearingAllowed(int b,int a,int c){return a<0||b==a||c<0||b==c;}
static float Score(int,Vec,Vec,Vec p,Vec d){return Distance(p,d);}
static bool Better(float score,int id,float old,int oldid){return old<0||score<old||(score==old&&id<oldid);}
static float TaskLifetimeSeconds(int,float distance){return 20+distance;}
};
struct ERR_HQRoadIntercept{static bool Select(ERR_HQBelief* b,float,Arr<Vec>& r,Vec& p){p.x=b->PredictedPosition.x+100+r.Count();return b->BeliefId%2==0;}};
struct Watch{bool ArmedSearchBlocked(int source,float){return source%11==0;}};
struct Coverage{bool allow=true;Arr<Vec> selections;bool Select(Vec p,float,Vec,float,Vec& result,Arr<Vec>& reserved){if(!allow)return false;result.x=p.x+reserved.Count()*25;selections.Insert(result);return true;}};
struct Network{ERR_HQRoute routes[129];ERR_HQRoute* Route(int radio,bool){return radio%7?&routes[radio]:nullptr;}};
struct Journal{int writes=0,seq=0;int Message(){return ++seq;}void Write(string,string,int){writes++;}};
struct Homes{std::map<int,int> v;void Set(int id,int n){v[id]=n;}int Get(int id){return v[id];}};
struct Harness {
Arr<ERR_HQUnit*> m_Units;Arr<ERR_HQBelief*> m_PlanBeliefs;Watch m_Watch;Coverage m_Coverage;Network m_Network;Journal m_Journal;Homes m_HomeAvailable;
std::map<int,ERR_HQHuntWindow> hunts;std::vector<std::unique_ptr<ERR_HQTask>> tasks;
int calls=0,visits=0,hunting=0;ERR_HQTask* NewTask(){tasks.push_back(std::make_unique<ERR_HQTask>());return tasks.back().get();}
COUNT
int Assigned(){calls++;visits+=m_Units.Count();return ActualAssigned();}
int HuntingAssigned(){return hunting;}
int HomeAvailable(ERR_HQUnit* u){return m_HomeAvailable.Get(u->HomeId);}
ERR_HQHuntWindow* Hunt(int id){return &hunts[id];}
bool AlarmCommitted(){for(auto u:m_Units)if(u->PendingTask&&u->PendingTask->BeliefId==63)return true;return false;}
Vec Containment(ERR_HQBelief*,Vec v){return v;}
void ReserveUnit(ERR_HQUnit* u,Arr<Vec>& reserved,int& first,int& second){if(!u->Assignment&&!u->PendingOrder)return;reserved.Insert(u->PlanPosition);int b=u->Id%2;if(first<0)first=b;else if(first!=b&&second<0)second=b;}
void Current(int globalLimit,int limit,float now){int assigned=0,radius=10000,roadQueries=0; CURRENT }
void Legacy(int globalLimit,int limit,float now){int assigned=0,radius=10000,roadQueries=0; LEGACY }
};
struct Setup{Harness h;ERR_HQUnit units[128];struct Group groups[128];ERR_HQReport reports[64];ERR_HQBelief beliefs[64];
Setup(int busy,int variant){
for(int i=0;i<128;i++){auto&u=units[i];u.Id=i+1;u.Radio=i+1;u.ForceSourceId=i+1;u.Group=&groups[i];u.PlanPosition={float(i+1)};u.HomeGuard=(i%5==0);u.HomeId=i%3;h.m_HomeAvailable.Set(u.HomeId,20);u.PlanEligible=variant!=1;u.PlanFighting=(i%17==0);if(i<busy){if(i%3!=1)u.Assignment=&reports[0];if(i%3!=0)u.PendingOrder=&reports[0];}h.m_Units.Insert(&u);}
for(int i=0;i<64;i++){auto&b=beliefs[i];b.BeliefId=i+1;b.Report=&reports[i];b.PredictedPosition={float(i*10)};b.Position=b.PredictedPosition;b.Confidence=.2f+.01f*i;b.Kind=i==62?ERR_HQMemoryKind::ALARM:(i%5==0?ERR_HQMemoryKind::GUNFIRE:0);h.m_PlanBeliefs.Insert(&b);h.hunts[i+1].NextHunt=(variant==2?100000:0);}
h.m_Coverage.allow=variant!=3;
}
};
void Equal(Setup&a,Setup&b){assert(a.h.m_Journal.writes==b.h.m_Journal.writes&&a.h.m_Coverage.selections==b.h.m_Coverage.selections&&a.h.m_HomeAvailable.v==b.h.m_HomeAvailable.v);
for(int i=0;i<128;i++){auto x=a.units[i].PendingTask,y=b.units[i].PendingTask;assert(bool(x)==bool(y));if(!x)continue;assert(x->SearchRadiusMetres==y->SearchRadiusMetres&&x->BeliefId==y->BeliefId&&x->Role==y->Role&&x->RoadIntercept==y->RoadIntercept&&x->Destination==y->Destination&&x->ExpiresAt==y->ExpiresAt&&x->ApproachBearing==y->ApproachBearing);}
for(auto [id,window]:a.h.hunts)assert(window.NextHunt==b.h.hunts[id].NextHunt);
}
int main(){
for(int variant=0;variant<4;variant++)for(int global:{0,1,2,3,8,16})for(int busy:{0,1,2,5,16})for(int hunting:{0,1,3}){
Setup current(busy,variant),legacy(busy,variant);current.h.hunting=legacy.h.hunting=std::min(busy,hunting);current.h.Current(global,std::min(global,3),1000);legacy.h.Legacy(global,std::min(global,3),1000);Equal(current,legacy);assert(current.h.calls<=1);assert(current.h.ActualAssigned()<=std::max(busy,global));}
Setup current(0,1),legacy(0,1);current.h.Current(16,3,1000);legacy.h.Legacy(16,3,1000);Equal(current,legacy);assert(current.h.visits==128);assert(legacy.h.visits>10000);
}
'''.replace('COUNT',adapt(assigned)).replace('CURRENT',adapt(code)).replace('LEGACY',adapt(legacy)))
assert s.index('int totalAssigned = Assigned();')>s.index('Investigate(now, globalLimit);')>s.index('Retask(now);')
print('PASS: actual full planner selection/commit body, 360 equivalence scenarios, orders/reservations/cooldowns identical; one128-unit scan vs >10000 repeated unit visits; no FPS claim')
@@ -0,0 +1,216 @@
"""Execute actual delivered-belief pursuit rules as C++; no engine/AI behavior claim."""
import os, re
from pathlib import Path
from policy_harness import ROOT, body, run
P = Path(os.environ.get('ERR_HQ_PURSUIT_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQPursuit.c'))
source = P.read_text()
# Exercise the real planner expression against the same actual policy methods.
HQ = Path(os.environ.get('ERR_HQ_SOURCE', ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'))
assignment = body(HQ, 'protected void Assign(')
selection = re.search(r'ERR_HQPursuitRole role = (ERR_HQPursuitRules\.Role\([^;]+\));', assignment)
assert selection, 'missing actual Assign role selection'
planner_role = selection.group(1).replace('ERR_HQPursuitRules.', 'Rules::')
methods = '\n'.join(body(P, signature) for signature in (
'static bool Finite(', 'static bool PositionValid(', 'static int Limit(', 'static bool Actionable(',
'static bool Heading(', 'static ERR_HQPursuitRole Role(', 'static float LifetimeSeconds(',
'static float SearchRadius(', 'static float TaskLifetimeSeconds(', 'static bool CanCommit(', 'static int Bearing(', 'static bool BearingAllowed(', 'static vector Direction(',
'static vector Objective(', 'static float Score(', 'static bool Better('))
methods = methods.replace('Math.', 'Math::').replace('vector.', 'vector::')
methods = methods.replace('ERR_HQPursuitRole.', 'ERR_HQPursuitRole::')
methods = methods.replace('vector offset;', 'vector offset{};')
run(r'''
#include <cassert>
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <limits>
#include <vector>
#include <set>
enum class ERR_HQPursuitRole { SEARCH, INTERCEPT, CONTAIN };
struct Math {
static float Clamp(float n,float lo,float hi){return std::clamp(n,lo,hi);}
static float AbsFloat(float n){return std::abs(n);}
static float Min(float a,float b){return std::min(a,b);}
};
struct vector {
float x=0,y=0,z=0;
vector()=default;
vector(float a,float b,float c):x(a),y(b),z(c){}
vector(const char* text){sscanf(text,"%f %f %f",&x,&y,&z);}
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;}
vector operator+(vector b)const{return {x+b.x,y+b.y,z+b.z};}
vector operator-(vector b)const{return {x-b.x,y-b.y,z-b.z};}
vector operator*(float v)const{return {x*v,y*v,z*v};}
float Length()const{return std::sqrt(x*x+y*y+z*z);}
vector Normalized()const{float n=Length();return {x/n,y/n,z/n};}
static float Dot(vector a,vector b){return a.x*b.x+a.y*b.y+a.z*b.z;}
static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}
};
vector Vector(float a,float b,float c){return {a,b,c};}
struct Rules {
'''+methods+r'''
};
struct Belief { float Confidence=.9f, Radius=50; vector Velocity; bool HasVelocity=true; };
ERR_HQPursuitRole PlannerRole(Belief belief,float age,int slot){return '''+planner_role+r''';}
bool near(float a,float b){return std::abs(a-b)<0.001f;}
int main(){
assert(Rules::SearchRadius(0)==45&&Rules::SearchRadius(44.99)==45&&Rules::SearchRadius(45)==45);
assert(Rules::SearchRadius(73)==73&&Rules::SearchRadius(110)==110&&Rules::SearchRadius(2000)==110);
assert(Rules::SearchRadius(-1)==0&&Rules::SearchRadius(std::numeric_limits<float>::quiet_NaN())==0&&Rules::SearchRadius(std::numeric_limits<float>::infinity())==0);
using Role=ERR_HQPursuitRole;
assert(Rules::Limit(-1)==0&&Rules::Limit(0)==0&&Rules::Limit(1)==1);
assert(Rules::Limit(3)==3&&Rules::Limit(8)==3);
assert(Rules::Actionable(.9,10,50)&&Rules::Actionable(.15,600,2000));
assert(!Rules::Actionable(.149,10,50)&&Rules::Actionable(.9,601,50));
assert(Rules::Actionable(.2,1200,1900)&&!Rules::Actionable(.2,1200,2001));
assert(!Rules::Actionable(.9,-1,50)&&!Rules::Actionable(.9,0,-1));
float nan=std::numeric_limits<float>::quiet_NaN();
float inf=std::numeric_limits<float>::infinity();
assert(!Rules::Actionable(nan,0,50)&&!Rules::Actionable(.9,inf,50));
assert(!Rules::Actionable(.9,0,nan)&&!Rules::Actionable(1.1,0,50));
vector north{0,0,5}, center{1000,12,1000};
assert(Rules::Heading(north,true)&&!Rules::Heading(north,false));
assert(!Rules::Heading({0,0,0},true)&&!Rules::Heading({0,0,71},true));
assert(!Rules::Heading({nan,0,5},true));
assert(Rules::Role(.9,10,50,true,0)==Role::INTERCEPT);
assert(Rules::Role(.9,10,50,true,1)==Role::SEARCH);
assert(Rules::Role(.9,10,50,true,2)==Role::CONTAIN);
// Memory can retain a valid slow-motion sample below the directed-task threshold.
// The actual Assign expression must not queue containment that delivery rejects.
Belief belief;
for(float speed:{0.f,.49f,70.01f})for(int slot=0;slot<3;slot++){
belief.Velocity={0,0,speed};
assert(PlannerRole(belief,10,slot)==Role::SEARCH);
}
for(float speed:{.5f,70.f}){
belief.Velocity={0,0,speed};
assert(PlannerRole(belief,10,0)==Role::INTERCEPT);
assert(PlannerRole(belief,10,1)==Role::SEARCH);
assert(PlannerRole(belief,10,2)==Role::CONTAIN);
}
belief.Velocity={nan,0,5};
assert(PlannerRole(belief,10,2)==Role::SEARCH);
belief.Velocity=north;belief.HasVelocity=false;
assert(PlannerRole(belief,10,0)==Role::SEARCH&&PlannerRole(belief,10,2)==Role::SEARCH);
for(int slot=0;slot<3;slot++){
assert(Rules::Role(.64,10,50,true,slot)==Role::SEARCH);
assert(Rules::Role(.9,46,50,true,slot)==Role::SEARCH);
assert(Rules::Role(.9,10,151,true,slot)==Role::SEARCH);
assert(Rules::Role(.9,10,50,false,slot)==Role::SEARCH);
}
assert(Rules::Role(.79,10,50,true,2)==Role::SEARCH);
assert(Rules::Role(.9,10,101,true,2)==Role::SEARCH);
assert(Rules::Role(.9,10,50,true,-1)==Role::SEARCH);
assert(Rules::LifetimeSeconds(Role::INTERCEPT)==60);
assert(Rules::LifetimeSeconds(Role::CONTAIN)==90);
assert(Rules::LifetimeSeconds(Role::SEARCH)==120);
assert(Rules::TaskLifetimeSeconds(Role::SEARCH,0)==120);
assert(near(Rules::TaskLifetimeSeconds(Role::SEARCH,2000),2000.f/3+45));
assert(Rules::TaskLifetimeSeconds(Role::SEARCH,5000)==900);
assert(Rules::TaskLifetimeSeconds(Role::INTERCEPT,2000)==60);
assert(Rules::TaskLifetimeSeconds(Role::CONTAIN,2000)==90);
assert(Rules::TaskLifetimeSeconds(Role::SEARCH,-1)==0);
assert(Rules::TaskLifetimeSeconds(Role::SEARCH,nan)==0);
assert(Rules::TaskLifetimeSeconds(Role::SEARCH,inf)==0);
// The caller must already own a physical, eligible and unassigned formation.
assert(Rules::CanCommit(true,true,false,false,0,0,3));
assert(!Rules::CanCommit(false,true,false,false,0,0,3));
assert(!Rules::CanCommit(true,false,false,false,0,0,3));
assert(!Rules::CanCommit(true,true,true,false,0,0,3));
assert(!Rules::CanCommit(true,true,false,false,0,-1,3));
assert(!Rules::CanCommit(true,true,false,false,0,0,0));
assert(!Rules::CanCommit(true,true,false,false,0,3,99));
assert(!Rules::CanCommit(true,true,false,true,1,0,3));
int homeAvailable=3,assigned=0;
for(int i=0;i<5;i++)if(Rules::CanCommit(true,true,false,true,homeAvailable,assigned,3)){
homeAvailable--;assigned++;
}
assert(homeAvailable==1&&assigned==2);
// Pending orders spend the slot immediately, and each busy unit is ineligible.
bool busy=false;int commitments=0;
for(int i=0;i<5;i++)if(Rules::CanCommit(true,true,busy,false,1,commitments,3)){
busy=true;commitments++;
}
assert(commitments==1);
vector intercept=Rules::Objective(center,north,true,.9,10,50,Role::INTERCEPT,0);
assert(near(intercept.x,1000)&&near(intercept.z,1065)&&near(intercept.y,12));
vector contain=Rules::Objective(center,north,true,.9,10,50,Role::CONTAIN,2);
assert(near(contain.x,1080)&&near(contain.z,1080));
vector stale=Rules::Objective(center,north,true,.4,180,400,Role::INTERCEPT,0);
assert(near(stale.z,1250)); // degraded to a bounded search sector, no velocity lead
for(int slot=-8;slot<9;slot++)for(float radius:{0.f,50.f,200.f,2000.f}){
vector objective=Rules::Objective(center,north,true,.2,300,radius,Role::SEARCH,slot);
assert(vector::DistanceXZ(center,objective)<=300&&near(objective.y,center.y));
}
vector maximum=Rules::Objective(center,{0,0,70},true,.9,0,50,Role::INTERCEPT,0);
assert(near(maximum.z,1200)); // future lead capped even for a fast vehicle
vector noDoubleAge=Rules::Objective(center,north,true,.9,40,50,Role::INTERCEPT,0);
assert(near(noDoubleAge.z,1060)); // supplied position is already memory-predicted
vector first=Rules::Objective(center,north,false,.4,180,100,Role::SEARCH,0);
vector second=Rules::Objective(center,north,false,.4,180,100,Role::SEARCH,1);
vector third=Rules::Objective(center,north,false,.4,180,100,Role::SEARCH,2);
assert(vector::DistanceXZ(first,second)>100&&vector::DistanceXZ(second,third)>100);
// Nearby units already ahead win interception; nearby correct-flank units contain.
float ahead=Rules::Score(Role::INTERCEPT,center,north,{1000,0,1200},intercept);
float behind=Rules::Score(Role::INTERCEPT,center,north,{1000,0,800},intercept);
assert(ahead>=0&&behind<0);
float right=Rules::Score(Role::CONTAIN,center,north,{1100,0,1050},contain);
float left=Rules::Score(Role::CONTAIN,center,north,{900,0,1050},contain);
assert(right<left);
assert(Rules::Score(Role::INTERCEPT,center,north,{intercept.x+150,0,intercept.z},intercept)>=0);
assert(Rules::Score(Role::INTERCEPT,center,north,{intercept.x+151,0,intercept.z},intercept)<0);
assert(Rules::Score(Role::CONTAIN,center,north,{contain.x+240,0,contain.z},contain)>=0);
assert(Rules::Score(Role::CONTAIN,center,north,{contain.x+241,0,contain.z},contain)<0);
assert(Rules::Score(Role::SEARCH,center,north,{1000,0,1000},first)
<Rules::Score(Role::SEARCH,center,north,{2000,0,1000},first));
assert(Rules::Score(Role::SEARCH,{nan,0,0},north,center,first)<0);
assert(Rules::Better(100,2,100,5)&&!Rules::Better(100,5,100,2));
assert(Rules::Better(99,5,100,2)&&Rules::Better(100,1,-1,-1));
assert(!Rules::Better(-1,1,100,2)&&!Rules::Better(nan,1,100,2));
int firstBearing=-1,secondBearing=-1;std::set<int> bearings;
std::vector<vector> forces={{1000,0,1200},{1200,0,1000},{1000,0,800},{800,0,1000}};
for(vector force:forces){
int bearing=Rules::Bearing(force,center);
if(!Rules::BearingAllowed(bearing,firstBearing,secondBearing))continue;
bearings.insert(bearing);
if(firstBearing<0)firstBearing=bearing;
else if(bearing!=firstBearing&&secondBearing<0)secondBearing=bearing;
}
assert(bearings.size()==2);
assert(Rules::BearingAllowed(firstBearing,firstBearing,secondBearing));
assert(!Rules::BearingAllowed(2,0,1)&&!Rules::BearingAllowed(3,0,1));
assert(!Rules::BearingAllowed(-1,-1,-1)&&!Rules::BearingAllowed(2,-1,1));
assert(Rules::Bearing({nan,0,0},center)==-1);
}
''')
for forbidden in ('GetGame(', 'GetOrigin(', 'GetPlayers(', 'GetControlledEntity(',
'Spawn(', 'CallLater(', 'SCR_AIGroup', 'IEntity'):
assert forbidden not in source, f'pursuit must consume snapshots only: {forbidden}'
assert all(len(line)<120 for line in source.splitlines())
print('PASS: actual bounded pursuit roles/objectives/scores, stale search degradation, physical/busy/home guards, finite lifetime and two-bearing escape gap; no hidden player reads or spawning')
for signature in ('protected void Assign(', 'protected void Retask('):
section=body(HQ,signature)
line=next(line.strip() for line in section.splitlines() if 'task.SearchRadiusMetres =' in line)
line=line.replace('ERR_HQPursuitRules.', 'Rules::').replace('ERR_HQPursuitRole.', 'Role::')
run(r"""
#include <cassert>
#include <algorithm>
#include <cmath>
#include <limits>
enum class Role {SEARCH,INTERCEPT,CONTAIN};
struct Math{static float Clamp(float n,float a,float b){return std::clamp(n,a,b);}};
struct Rules{
"""+body(P,'static bool Finite(')+body(P,'static float SearchRadius(').replace('Math.','Math::')+r"""
};
struct Task{float SearchRadiusMetres=0;};struct Belief{float Radius=0;};
void Queue(Task& task,Belief& belief,Role role,bool roadIntercept){
"""+line+r"""
}
int main(){for(float radius:{0.f,60.f,500.f}){Task task;Belief belief{radius};Queue(task,belief,Role::SEARCH,false);float copied=Rules::SearchRadius(radius);assert(task.SearchRadiusMetres==copied);belief.Radius=1900;assert(task.SearchRadiusMetres==copied);}
"""+(r"""for(Role role:{Role::INTERCEPT,Role::CONTAIN}){Task task;Belief belief{100};Queue(task,belief,role,false);assert(task.SearchRadiusMetres==0);}Task road;Belief belief{100};Queue(road,belief,Role::SEARCH,true);assert(road.SearchRadiusMetres==0);""" if 'Assign' in signature else '')+r"""
}
""")
print('PASS: actual finite45..110 radius and both ordinary search queue snapshot expressions')