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:
@@ -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')
|
||||
Reference in New Issue
Block a user