Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
70 lines
7.0 KiB
Python
70 lines
7.0 KiB
Python
"""Execute real convoy flank planning and bounded search continuation."""
|
|
import re
|
|
from policy_harness import ROOT, body, run
|
|
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoyTactics.c'
|
|
s='\n'.join(body(p,signature) for signature in ('protected bool PlanFlank(', 'protected void ContinueSearch(', 'void Report('))
|
|
s=s.replace('protected ','').replace('vector ','Vec ')
|
|
for typ in ('ERR_SupplyTeam','AIWaypoint','IEntity'):
|
|
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
|
|
s=s.replace('GetGame().GetWorld().GetWorldTime()', '0')
|
|
s=s.replace('AIWaypoint* exit;', 'AIWaypoint* exit=nullptr;')
|
|
for name in ('m_Task','team','move','search','leader'):
|
|
s=s.replace(name+'.',name+'->')
|
|
s=s.replace('Group.','Group->').replace('Vehicles[2].','Vehicles[2]->')
|
|
for typ in ('SCR_BoardingWaypoint','AIWaypoint','ERR_Actors','ERR_Resources','SCR_EntityHelper','ERR_ForceRules','vector'):
|
|
s=s.replace(typ+'.',('Geometry' if typ=='vector' else typ)+'::')
|
|
s=re.sub(r' ERR_HQ.Event\([^\n]+\n','',s)
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <cmath>
|
|
#include <vector>
|
|
struct Vec{float x=0,y=0,z=0;Vec operator+(Vec b){return {x+b.x,y+b.y,z+b.z};}Vec operator-(Vec b){return {x-b.x,y-b.y,z-b.z};}Vec operator*(float n){return {x*n,y*n,z*n};}};
|
|
namespace Geometry{float DistanceXZ(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);}}
|
|
namespace ERR_ForceRules{bool Finite(float f){return std::isfinite(f);}bool FiniteVector(Vec p){return Finite(p.x)&&Finite(p.y)&&Finite(p.z);}}
|
|
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}};
|
|
struct IEntity{Vec position;Vec GetOrigin(){return position;}};
|
|
struct AIWaypoint{bool boarding=false,deleted=false;Vec position;float radius=0;void SetCompletionRadius(float f){radius=f;}static AIWaypoint* Cast(AIWaypoint* p){return p;}};
|
|
namespace SCR_BoardingWaypoint{AIWaypoint* Cast(AIWaypoint* p){return p&&p->boarding?p:nullptr;}}
|
|
namespace Alloc{Array<AIWaypoint*> all;bool failMove=false,failSearch=false,failExit=false;AIWaypoint* New(Vec p,bool boarding=false){auto* w=new AIWaypoint;w->position=p;w->boarding=boarding;all.push_back(w);return w;}}
|
|
namespace ERR_Resources{int SearchWaypoint(){return 1;}}
|
|
namespace ERR_Actors{AIWaypoint* Spawn(int,Vec p){return Alloc::failSearch?nullptr:Alloc::New(p);}}
|
|
namespace SCR_EntityHelper{void DeleteEntityAndChildren(AIWaypoint* p){p->deleted=true;}}
|
|
struct Group{IEntity leader;bool hasLeader=true;AIWaypoint* current=nullptr;AIWaypoint* GetCurrentWaypoint(){return current;}IEntity* GetLeaderEntity(){return hasLeader?&leader:nullptr;}};
|
|
struct ERR_SupplyTeam{struct Group* Group=nullptr;bool Released=false,living=true,aboard=false,SearchPlanned=false;Vec OrderedContact,SearchPosition;int SearchStep=0,cleared=0;Array<AIWaypoint*> Orders;bool Living(){return living;}bool Aboard(IEntity*,bool required){return aboard==required;}void Clear(){cleared++;for(auto* p:Orders)p->deleted=true;Orders.clear();}void Add(AIWaypoint* w){if(w)Orders.push_back(w);}};
|
|
struct Vehicle{IEntity* Entity=nullptr;};struct Task{Array<ERR_SupplyTeam*> Teams;Array<Vehicle*> Vehicles;};
|
|
struct Tactics{Task* m_Task;Vec m_Contact={1000,0,1000},m_Forward={0,0,1},m_Left={-1,0,0};float m_NextSearch=0,m_LastContact=-1,m_NextOrders=0;bool m_Stopped=false,foot=true;int m_State=1,starts=0,orders=0;void Command(AIWaypoint*){}void Claim(Group*,Vec,float){}bool FootPosition(ERR_SupplyTeam*,Vec desired,Vec& out){out=desired;return foot;}AIWaypoint* Move(Vec p,float r){if(Alloc::failMove)return nullptr;auto* w=Alloc::New(p);w->radius=r;return w;}AIWaypoint* Board(IEntity*,bool){return Alloc::failExit?nullptr:Alloc::New({},true);}void Start(Vec p,float){starts++;m_Contact=p;m_State=1;}void Orders(bool,float){orders++;}
|
|
'''+s+r'''
|
|
};
|
|
int main(){
|
|
Task task;Vehicle truck;IEntity vehicle;truck.Entity=&vehicle;task.Vehicles={&truck,&truck,&truck};Group leftGroup,rightGroup;ERR_SupplyTeam left,right;left.Group=&leftGroup;right.Group=&rightGroup;task.Teams={&left,&right};Tactics t;t.m_Task=&task;
|
|
assert(t.PlanFlank(0,false,0)&&t.PlanFlank(1,false,0));
|
|
assert(left.Orders.size()==2&&right.Orders.size()==2);
|
|
assert(left.SearchPosition.x==930&&right.SearchPosition.x==1070&&left.SearchPosition.z==1000);
|
|
assert(left.Orders[0]->position.x==890&&left.Orders[0]->position.z==920); // separated approach around last known position
|
|
auto* kept=left.Orders[0];int cleared=left.cleared;
|
|
t.foot=false;assert(!t.PlanFlank(0,false,1)&&left.Orders[0]==kept&&left.cleared==cleared);t.foot=true;
|
|
Alloc::failSearch=true;int n=Alloc::all.size();assert(!t.PlanFlank(0,false,1)&&left.Orders[0]==kept);assert(Alloc::all[n]->deleted);Alloc::failSearch=false;
|
|
Alloc::failMove=true;n=Alloc::all.size();assert(!t.PlanFlank(0,false,1)&&left.Orders[0]==kept);assert(Alloc::all[n]->deleted);Alloc::failMove=false;
|
|
left.aboard=true;Alloc::failExit=true;n=Alloc::all.size();assert(!t.PlanFlank(0,true,0)&&left.Orders[0]==kept);assert(int(Alloc::all.size())==n);Alloc::failExit=false;
|
|
assert(t.PlanFlank(0,true,0)&&left.Orders.size()==1&&left.Orders[0]->boarding&&!left.SearchPlanned);
|
|
leftGroup.current=left.Orders[0];kept=left.Orders[0];assert(!t.PlanFlank(0,true,0)&&left.Orders[0]==kept); // preserve native dismount in progress
|
|
leftGroup.current=nullptr;left.aboard=false;assert(t.PlanFlank(0,false,0)&&left.Orders.size()==2&&!left.Orders[0]->boarding);
|
|
leftGroup.leader.position=left.SearchPosition;rightGroup.leader.position={0,0,0};
|
|
t.ContinueSearch(100);assert(left.SearchStep==1&&right.SearchStep==0);assert(left.SearchPosition.x==860&&left.SearchPosition.z==1100);
|
|
leftGroup.leader.position=left.SearchPosition;t.ContinueSearch(45099);assert(left.SearchStep==1);
|
|
t.ContinueSearch(45100);assert(left.SearchStep==2&&left.SearchPosition.x==950&&left.SearchPosition.z==1160);
|
|
cleared=left.cleared;leftGroup.leader.position=left.SearchPosition;t.ContinueSearch(90100);assert(left.SearchStep==2&&left.cleared==cleared); // finite search, no churn
|
|
t.m_Contact.x+=100;t.ContinueSearch(135100);assert(left.SearchStep==0&&left.OrderedContact.x==1100); // a refused earlier redirect retries from new contact
|
|
left.SearchPlanned=false;left.Released=true;cleared=left.cleared;t.ContinueSearch(180100);assert(left.cleared==cleared);
|
|
left.Released=false;left.living=false;t.ContinueSearch(225100);assert(left.cleared==cleared);
|
|
left.living=true;left.aboard=true;t.ContinueSearch(270100);assert(left.cleared==cleared);
|
|
left.aboard=false;t.ContinueSearch(315100);assert(left.SearchPlanned&&left.cleared==cleared+1);
|
|
t.m_LastContact=100;t.m_NextOrders=0;Vec newer={2000,0,2000};t.Report(newer,100,200);assert(t.orders==0&&t.m_Contact.x==1100);
|
|
t.Report(newer,99,200);assert(t.orders==0);t.Report(newer,201,200);assert(t.orders==0);
|
|
t.Report(newer,101,200);assert(t.orders==1&&t.m_Contact.x==2000&&t.m_LastContact==101);
|
|
t.Report({},101,300);assert(t.orders==1&&t.m_LastContact==101); // replay cannot reset search or redirect
|
|
for(auto* p:Alloc::all)delete p;
|
|
}
|
|
''')
|
|
print('PASS: opposing native flank orders are transactional; arrived teams sweep twice, travelling/boarding teams retain orders, failed redirects retry and replayed contacts are ignored')
|