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

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')