Files
EscapistsReintroduced/tests/test_hunter_tactics.py
T

249 lines
24 KiB
Python

"""Execute production formation geometry and tactical lifecycle decisions."""
import re
from policy_harness import ROOT,body,run
p=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterTactics.c'
geometry='\n'.join(body(p,x) for x in ['static float ManeuverBudget(', 'static bool ExtendApproach(', 'static bool AdvanceWithoutVisualCover(', 'static bool DismountRange(', 'static bool Fresh(', 'static vector Offset(', 'static bool Stage(', 'static bool Foot('])
geometry=geometry.replace('array<vector> occupied','Array<Vec> occupied').replace('foreach (vector other : occupied)','for (Vec other : occupied)')
geometry=re.sub(r'\bvector (?=\w)','Vec ',geometry).replace('vector.','Geometry::').replace('Vector(','Vec(').replace('Math.Clamp','std::clamp').replace('30 + metres / 2, 90, 240','30 + metres / 2, 90.f, 240.f')
stubs=r'''
#include <cassert>
#include <algorithm>
#include <cmath>
#include <string>
#include <vector>
using string=std::string;
struct Vec {float x=0,y=0,z=0;Vec(){}Vec(float a,float b,float c):x(a),y(b),z(c){}float& operator[](int n){return n==0?x:n==1?y:z;}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};}float Length(){return std::hypot(x,z);}void Normalize(){float length=Length();x/=length;z/=length;}string ToString(){return "position";}};
namespace Geometry{float DistanceXZ(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);}float Dot(Vec a,Vec b){return a.x*b.x+a.y*b.y+a.z*b.z;}}
template<class T>using Array=std::vector<T>;
'''
run(stubs+geometry+r'''
int main(){
assert(DismountRange(140)&&DismountRange(450)&&!DismountRange(139)&&!DismountRange(451));
assert(ExtendApproach(1000,91000,91000));
assert(!ExtendApproach(1000,75999,91000));
assert(!ExtendApproach(1000,301000,301000));
assert(!ExtendApproach(1000,92000,91000));
assert(!ExtendApproach(92000,90000,91000));
assert(!AdvanceWithoutVisualCover(true,47000,16999));
assert(AdvanceWithoutVisualCover(true,47000,17000));
assert(!AdvanceWithoutVisualCover(false,47000,47000));
assert(ManeuverBudget(0)==90000&&ManeuverBudget(300)==180000&&ManeuverBudget(10000)==240000);
assert(Fresh(0,60000)&&!Fresh(0,60001)&&!Fresh(100,99)&&!Fresh(-1,0));
Vec c{300,0,500};
for(int a=0;a<360;a+=15){Vec f{std::sin(a*3.14159265f/180),0,std::cos(a*3.14159265f/180)};
Vec cover=Offset(c,f,0,0),left=Offset(c,f,1,0),right=Offset(c,f,2,0);
assert(Stage(cover,c,f,{}));assert(Stage(left,c,f,{cover}));assert(Stage(right,c,f,{cover,left}));
assert(!Stage(left,c,f,{left}));assert(!Stage(c,c,f,{}));assert(!Stage(c+f*300,c,f,{}));
for(int role=1;role<3;role++){assert(Foot(Offset(c,f,role,1),c,f,role));assert(Foot(Offset(c,f,role,2),c,f,role));assert(!Foot(Offset(c,f,3-role,2),c,f,role));}
}
}
''')
tick='\n'.join(body(p,x) for x in ['protected bool RecoverApproach(', 'protected bool UpdateCover(', 'bool Tick(ERR_HQ hq, float now)']).replace('protected ', '').replace('bool assigned;', 'bool assigned=false;').replace('m_ApproachRetries.ToString()', 'std::to_string(m_ApproachRetries)').replace('assigned.ToString()', 'std::to_string(assigned)')
for typ in ['ERR_HQ','IEntity','AIWaypoint']:
tick=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',tick)
for n in ['Radio','hq','Unit','Attack','search','coordination','m_GunTeam']:
tick=tick.replace(n+'.',n+'->')
tick=tick.replace('ERR_HQ.Get()', 'ERR_HQ::Get()').replace('vector reserved', 'Vec reserved').replace('AttackSpace().Claim', 'AttackSpace()->Claim')
tick=tick.replace('foreach (IEntity* crew : Unit->Members)','for (IEntity* crew : Unit->Members)')
tick=tick.replace('Unit->Entity.GetOrigin()','Unit->Entity->GetOrigin()')
tick=tick.replace('ERR_HunterAttackRules.', '').replace('Math.Min', 'std::min').replace('now + 30000, m_ApproachStarted + 300000','now + 30000.f, m_ApproachStarted + 300000.f')
tick=tick.replace('AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.SearchWaypoint(), Sector))','SpawnSearch(Sector)')
tick=tick.replace('ERR_ConvoyMountedCrew.Hold','Hold').replace('ERR_HunterCombat.Active','Active').replace('vector.DistanceXZ','Geometry::DistanceXZ').replace('null','nullptr')
run(stubs+body(p,'static bool DismountRange(')+body(p,'static bool ExtendApproach(')+body(p,'static bool AdvanceWithoutVisualCover(')+body(p,'static float ManeuverBudget(').replace('Math.Clamp','std::clamp').replace('30 + metres / 2, 90, 240','30 + metres / 2, 90.f, 240.f')+r'''
struct IEntity{Vec pos;Vec GetOrigin(){return pos;}};
struct AIWaypoint{void SetCompletionRadius(float){}};
struct Space{void Claim(void*,Vec,float){}};struct ERR_HQ{void CoordinatedHunterDelivery(string,int,bool,float){}Space space;static ERR_HQ*Get(){return nullptr;}Space*AttackSpace(){return &space;}int Journal(){return 0;}};
struct RadioType{int MessageId=1;int result=0;int Advance(float,int){return result;}};
struct GroupType{bool active=false;};bool Active(GroupType*g){return g->active;}
int holds=0;void Hold(IEntity*,IEntity*){holds++;}
struct UnitType{string Source="hunter";struct GroupType*Group=nullptr;IEntity*Entity=nullptr;Array<IEntity*>Members;void*Downlink=nullptr;void CancelHQDownlink(){Downlink=nullptr;}bool CombatHeld=false;float CombatQuietUntil=0;int clears=0;void ClearOrder(){clears++;}};
struct AttackType{Vec ReportPosition;float Started=0;bool alive=true,ready=false;bool CoverAlive(){return alive;}bool CoverReady(){return ready;}};
struct GunTeam{int ticks=0;void Tick(float){ticks++;}void Pause(){}};
struct Tactics{
int rejected=0;void RejectStage(float){rejected++;}
int m_ApproachRetries=0,m_CoverRetries=0,alternates=0;float m_BlindSince=-1;bool alternateWorks=true;
bool AlternateStage(float now){alternates++;if(!alternateWorks)return false;State=1;m_BlindSince=-1;m_ApproachProgress=now;Stage={500,0,-250};return true;}
GunTeam*m_GunTeam=nullptr;bool align=false,facing=false;bool TryAlign(){return align;}bool FacingSector(){return facing;}
UnitType*Unit=nullptr;AttackType*Attack=nullptr;RadioType*Radio=nullptr;int Role=1,State=0,Retries=0;float m_TravelDistance=0,Deadline=0,NextRetry=0,m_ApproachStarted=0,m_ApproachProgress=0;Vec m_ApproachPosition;bool m_UnsupportedSearch=false,Delivered=false,Stopped=false,living=true,aboard=true,moveWorks=true,boardWorks=true,searchWorks=true;int clears=0,moves=0,boards=0;Vec Stage,Approach,Sector,position;AIWaypoint search;
bool visual=false;bool VisualCover(){return visual;}bool Living(){return living;}void Stop(){Stopped=true;Radio=nullptr;Clear();if(Delivered)Unit->CombatHeld=false;}bool Move(Vec,float=20){moves++;return moveWorks;}void Clear(){clears++;}bool Board(bool){boards++;return boardWorks;}bool Aboard(bool required){return aboard==required;}Vec Position(){return position;}void VehicleUse(bool){}void Note(string){}void ObserveState(float){}void Add(AIWaypoint*){}AIWaypoint*SpawnSearch(Vec){return searchWorks?&search:nullptr;}
'''+tick+r'''
};
struct Fixture{IEntity car,crew;GroupType group;UnitType unit;AttackType attack;RadioType radio;Tactics t;ERR_HQ hq;
Fixture(){car.pos={0,0,-250};unit.Entity=&car;unit.Group=&group;unit.Members={&crew};t.Unit=&unit;t.Attack=&attack;t.Radio=&radio;t.Stage=car.pos;t.Approach={-170,0,-85};t.Sector={-105,0,35};t.position=car.pos;}
};
int main(){
{Fixture f;f.radio.result=1;f.t.Role=0;f.t.Tick(&f.hq,1000);assert(f.t.State==3);
f.t.Tick(&f.hq,20999);assert(f.t.alternates==0);f.t.Tick(&f.hq,21000);assert(f.t.alternates==1&&f.t.State==1);}
{Fixture f;f.radio.result=1;f.t.Role=0;f.t.visual=true;f.t.Tick(&f.hq,1000);f.t.Tick(&f.hq,120000);assert(f.t.State==3&&f.t.alternates==0);}
{Fixture f;f.radio.result=1;f.t.Role=0;f.t.alternateWorks=false;f.t.Tick(&f.hq,1000);
f.t.Tick(&f.hq,21000);f.t.Tick(&f.hq,22000);assert(f.t.alternates==1);
f.t.Tick(&f.hq,41000);f.t.Tick(&f.hq,61000);assert(f.t.alternates==3&&!f.t.Stopped);
assert(!f.t.Tick(&f.hq,81000)&&f.t.Stopped);}
{Fixture f;f.radio.result=1;f.t.Role=0;f.t.alternateWorks=false;f.t.m_TravelDistance=1000;f.car.pos={0,0,-1100};f.t.Stage={0,0,-210};
f.t.Tick(&f.hq,1000);f.t.Tick(&f.hq,30999);assert(f.t.moves==1);
f.t.Tick(&f.hq,31000);assert(f.t.moves==2&&f.t.m_ApproachRetries==1);
f.t.Tick(&f.hq,61000);assert(f.t.alternates==1&&f.t.m_ApproachRetries==2);
assert(!f.t.Tick(&f.hq,91000)&&f.t.Stopped);}
{Fixture f;f.radio.result=1;f.t.Role=0;f.t.m_TravelDistance=1000;f.car.pos={0,0,-1100};f.t.Stage={0,0,-210};
f.t.Tick(&f.hq,1000);for(int i=1;i<=5;i++){f.car.pos.z+=20;f.t.Tick(&f.hq,1000+i*20000);}assert(f.t.m_ApproachRetries==0&&!f.t.Stopped);}
{Fixture f;f.radio.result=1;f.t.Role=0;f.t.Stage={0,0,-500};f.t.visual=true;
assert(f.t.Tick(&f.hq,1000)&&f.t.State==3); // sees fight at 250m: stop, don't back away to stage
holds=0;
}
{Fixture f;assert(!f.t.Tick(&f.hq,1000));assert(!f.t.Delivered&&!f.unit.CombatHeld&&!f.unit.clears&&f.t.moves==0);f.radio.result=-1;assert(!f.t.Tick(&f.hq,2000)&&f.t.Stopped&&f.unit.clears==0);}
{Fixture f;f.radio.result=1;f.t.moveWorks=false;assert(!f.t.Tick(&f.hq,1000)&&!f.t.Delivered&&f.unit.clears==0);}
{Fixture f;f.radio.result=1;assert(f.t.Tick(&f.hq,1000));assert(f.t.State==2&&f.t.boards==1&&f.unit.clears==1&&f.unit.CombatHeld);f.t.aboard=false;assert(f.t.Tick(&f.hq,2000)&&f.t.State==3);int moves=f.t.moves;f.t.Tick(&f.hq,5000);assert(f.t.moves==moves);f.attack.ready=true;f.t.Tick(&f.hq,6000);assert(f.t.State==4&&f.t.moves==moves+1);f.t.Tick(&f.hq,7000);assert(f.t.moves==moves+1);f.t.position=f.t.Approach;f.t.Tick(&f.hq,8000);assert(f.t.State==5);f.t.Tick(&f.hq,68000);assert(f.t.State==6);f.t.aboard=true;assert(!f.t.Tick(&f.hq,69000)&&f.t.Stopped&&!f.unit.CombatHeld);}
{Fixture f;f.radio.result=1;f.t.Tick(&f.hq,1000);f.t.aboard=false;f.t.Tick(&f.hq,2000);f.t.Tick(&f.hq,16999);assert(f.t.State==3);f.t.Tick(&f.hq,17000);assert(f.t.State==4&&f.t.moves==2);}
{Fixture f;f.radio.result=1;f.t.Tick(&f.hq,1000);f.t.aboard=false;f.t.Tick(&f.hq,2000);f.attack.alive=false;f.t.Tick(&f.hq,47000);assert(f.t.State==4&&f.t.moves==2&&f.t.m_UnsupportedSearch);}
{Fixture f;f.radio.result=1;f.car.pos=f.t.position={0,0,-1000};f.t.Stage={1000,0,1000};f.t.Tick(&f.hq,1000);assert(f.t.State==1);f.car.pos={0,0,-950};f.t.Tick(&f.hq,91000);assert(!f.t.Stopped&&f.t.Deadline==121000);f.t.Tick(&f.hq,121000);assert(f.t.Stopped);}
{Fixture f;f.radio.result=1;f.car.pos=f.t.position={0,0,-1000};f.t.Stage={1000,0,1000};f.t.Tick(&f.hq,1000);f.car.pos={0,0,-950};f.t.Tick(&f.hq,301000);assert(f.t.Stopped);}
{Fixture f;f.radio.result=1;f.t.Tick(&f.hq,1000);f.t.aboard=false;f.attack.ready=true;f.t.Tick(&f.hq,2000);assert(f.t.State==4);f.attack.alive=false;f.t.Tick(&f.hq,3000);assert(f.t.State==3);}
{Fixture f;f.t.Role=0;f.radio.result=1;f.t.Tick(&f.hq,1000);assert(f.t.State==3&&f.t.boards==0&&holds==1);assert(!f.t.Tick(&f.hq,300000)&&f.t.Stopped);}
{Fixture f;f.radio.result=1;f.t.Stage={1000,0,1000};f.t.Tick(&f.hq,1000);assert(f.t.State==2); } // Road passes within infantry range before exact stage.
{Fixture f;f.radio.result=1;f.t.Tick(&f.hq,1000);f.t.aboard=false;f.t.Tick(&f.hq,2000);f.attack.alive=false;f.t.Tick(&f.hq,47000);assert(f.t.State==4&&f.t.m_UnsupportedSearch);f.t.Tick(&f.hq,48000);assert(f.t.State==4); } // No repeated cover-loss reset.
{Fixture f;f.radio.result=1;f.t.Tick(&f.hq,1000);f.t.aboard=false;f.t.Tick(&f.hq,2000);f.attack.alive=false;f.t.moveWorks=false;f.t.Tick(&f.hq,47000);assert(f.t.State==6); } // Failed foot order still regroups.
{Fixture f;f.radio.result=1;f.t.align=true;f.t.Tick(&f.hq,1000);assert(f.t.State==8&&f.t.boards==0);f.t.Tick(&f.hq,20999);assert(f.t.State==8);f.t.Tick(&f.hq,21000);assert(f.t.State==2&&f.t.boards==1);} // Alignment never blocks beyond 20s.
{Fixture f;f.radio.result=1;f.t.align=true;f.t.Tick(&f.hq,1000);f.t.facing=true;f.t.Tick(&f.hq,2000);assert(f.t.State==2);} // Stop aligning when facing.
{Fixture f;f.radio.result=1;f.t.align=true;f.t.Tick(&f.hq,1000);f.group.active=true;f.t.Tick(&f.hq,2000);assert(f.t.State==2);} // Combat preempts alignment.
{Fixture f;f.radio.result=1;f.t.Tick(&f.hq,1000);f.t.living=false;assert(!f.t.Tick(&f.hq,2000)&&f.t.Stopped);}
}
''')
print('PASS: separated rear vehicle approaches, opposite infantry sectors, radio-gated ownership, failed-order preservation, visual cover or bounded supported reconnaissance, bounded dismount/search/regroup and loss cleanup')
# Execute the ownership methods too: an interrupted flank must restore only its
# own vehicle and dispose only its own waypoints, including allocation failures.
own='\n'.join(body(p,x) for x in ['protected void Clear(', 'protected void Add(', 'protected void VehicleUse(', 'protected bool Board(', 'void Stop()'])
own=own.replace('protected ','').replace('foreach (','for (')
for typ in ['AIWaypoint','SCR_BoardingWaypoint','SCR_AIGroupUtilityComponent','SCR_AIVehicleUsageComponent','IEntity','ERR_HQ']:
own=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',own)
for name in ['Unit','utility','waypoint','m_Mode','coordination','pendingHQ','Radio','m_GunTeam']:
own=own.replace(name+'.',name+'->')
own=own.replace('ERR_HQ.Get()', 'ERR_HQ::Get()').replace('AttackSpace().Release', 'AttackSpace()->Release')
own=own.replace('Unit->Group.','Unit->Group->').replace('Unit->Entity.FindComponent(SCR_AIVehicleUsageComponent)','Unit->Entity->usage')
own=own.replace('SCR_AIVehicleUsageComponent.Cast','UsageCast').replace('SCR_BoardingWaypoint.Cast','BoardCast')
for cls in ['ERR_ConvoyRules','ERR_SupplyConvoyTactics','ERR_ConvoyMountedCrew','SCR_EntityHelper']:
own=own.replace(cls+'.',cls+'::')
own=own.replace('Unit->Recovery.SuspendBoarding()', 'Unit->Recovery->SuspendBoarding()')
own=own.replace('SCR_AIActionBase.PRIORITY_LEVEL_PLAYER','1000').replace('null','nullptr')
run(r'''
#include <cassert>
#include <algorithm>
#include <vector>
template<class T>struct Array:std::vector<T>{void Insert(T v){this->push_back(v);}void Clear(){this->clear();}};
struct AIWaypoint{bool boarding=true;int deleted=0;};
struct SCR_BoardingWaypoint:AIWaypoint{int priority=0;bool driver=false,turret=false,passenger=false;void SetPriorityLevel(int n){priority=n;}void SetAllowance(bool a,bool b,bool c){driver=a;turret=b;passenger=c;}};
SCR_BoardingWaypoint*BoardCast(AIWaypoint*w){return w&&w->boarding?static_cast<SCR_BoardingWaypoint*>(w):nullptr;}
struct SCR_AIVehicleUsageComponent{};auto UsageCast(SCR_AIVehicleUsageComponent*u){return u;}
struct SCR_AIGroupUtilityComponent{int adds=0,removes=0;void AddUsableVehicle(SCR_AIVehicleUsageComponent*){adds++;}void RemoveUsableVehicle(SCR_AIVehicleUsageComponent*){removes++;}};
struct Group{SCR_AIGroupUtilityComponent utility;Array<AIWaypoint*>waypoints;auto GetGroupUtilityComponent(){return &utility;}void RemoveWaypoint(AIWaypoint*w){auto&i=waypoints;i.erase(std::remove(i.begin(),i.end(),w),i.end());}void AddWaypoint(AIWaypoint*w){waypoints.Insert(w);}};
struct IEntity{SCR_AIVehicleUsageComponent usageValue;SCR_AIVehicleUsageComponent*usage=&usageValue;};
struct Recovery{void SuspendBoarding(){}};
struct UnitType{const char*Source="hunter";struct Recovery*Recovery=nullptr;struct Group*Group=nullptr;IEntity*Entity=nullptr;Array<IEntity*>Members;bool CombatHeld=true;float NextOrder=99;};
struct Mode{bool active=true;bool Active(){return active;}};
namespace ERR_ConvoyRules{bool captured=false;bool Captured(IEntity*,Group*){return captured;}}
namespace ERR_ConvoyMountedCrew{int released=0;void Release(IEntity*){released++;}}
namespace SCR_EntityHelper{void DeleteEntityAndChildren(AIWaypoint*w){w->deleted++;}}
namespace ERR_SupplyConvoyTactics{AIWaypoint*next=nullptr;bool requested=false;AIWaypoint*Board(IEntity*,bool enter){requested=enter;return next;}}
struct World{float GetWorldTime(){return 0;}};struct Game{World GetWorld(){return {};}};Game GetGame(){return {};}
struct RadioWire{int MessageId=1;};
struct Space{void Release(void*){}};struct ERR_HQ{void CoordinatedHunterDelivery(const char*,int,bool,float){}Space space;static ERR_HQ*Get(){return nullptr;}Space*AttackSpace(){return &space;}};
struct ERR_HunterGunTeam{inline static bool usable=false;int stopped=0;ERR_HunterGunTeam(UnitType*){}bool Start(){return usable;}void Stop(){stopped++;}};
struct Tactics{ERR_HunterGunTeam*m_GunTeam=nullptr;void Note(const char*){}UnitType*Unit=nullptr;Mode*m_Mode=nullptr;int Role=0;bool m_VehicleDetached=false,Stopped=false,Delivered=false,living=true;RadioWire*Radio=nullptr;Array<AIWaypoint*>Orders;bool Living(){return living&&!Stopped;}
'''+own+r'''
};
int main(){
Group g;IEntity car,man;UnitType u;u.Group=&g;u.Entity=&car;u.Members.Insert(&man);Mode mode;Tactics t;t.Unit=&u;t.m_Mode=&mode;
AIWaypoint unrelated,old;g.AddWaypoint(&unrelated);t.Add(&old);
assert(!t.Board(false)&&t.Orders.size()==1&&old.deleted==0&&g.waypoints.size()==2);
AIWaypoint wrong;wrong.boarding=false;ERR_SupplyConvoyTactics::next=&wrong;
assert(!t.Board(false)&&wrong.deleted==1&&old.deleted==0);
SCR_BoardingWaypoint exit;ERR_SupplyConvoyTactics::next=&exit;
assert(t.Board(false)&&!ERR_SupplyConvoyTactics::requested&&old.deleted==1);
assert(exit.priority==1000&&exit.driver&&exit.turret&&exit.passenger&&unrelated.deleted==0);
t.VehicleUse(false);t.VehicleUse(false);assert(g.utility.removes==1&&t.m_VehicleDetached);
t.Delivered=true;t.Stop();assert(t.Stopped&&g.utility.adds==1&&exit.deleted==1&&unrelated.deleted==0&&g.waypoints.size()==1&&!u.CombatHeld&&u.NextOrder==0);
t.Stop();assert(g.utility.adds==1&&exit.deleted==1&&ERR_ConvoyMountedCrew::released==1);
ERR_HunterGunTeam::usable=true;Tactics split;split.Unit=&u;split.m_Mode=&mode;assert(split.Board(false)&&split.m_GunTeam);auto held=split.m_GunTeam;
assert(split.Board(false)&&split.m_GunTeam==held);split.Stop();assert(held->stopped==1&&!split.m_GunTeam);split.Stop();assert(held->stopped==1);ERR_HunterGunTeam::usable=false;
int added=g.utility.adds;
for(int scenario=0;scenario<3;scenario++){
Tactics x;x.Unit=&u;x.m_Mode=&mode;x.m_VehicleDetached=true;
ERR_ConvoyRules::captured=scenario==0;mode.active=scenario!=1;x.living=scenario!=2;
x.Stop();assert(g.utility.adds==added); // Never reclaim a captured/dead/teardown vehicle.
}
}
''')
print('PASS: production dismount/boarding allocation, all crew allowances, waypoint ownership, one-time vehicle detach/restore and capture/teardown guards')
armed=(body(p,'static bool CoverCrew(').replace('ERR_HunterVehicle unit','UnitType* unit').replace('ERR_GameMode mode','int mode')+'\n'+body(p,'bool ArmedCover()')).replace('foreach (','for (').replace('BaseCompartmentSlot seat','BaseCompartmentSlot* seat').replace('bool gunner;','bool gunner=false;')
for name in ['Unit','unit','seat','m_GunTeam']:armed=armed.replace(name+'.',name+'->')
armed=armed.replace('ECompartmentType.TURRET','1').replace('ERR_Actors.CanFight','CanFight')
armed=armed.replace('ERR_HQMountedAmmoSnapshot ammo','ERR_HQMountedAmmoSnapshot* ammo').replace('ERR_HQMountedAmmoSampler.Inspect','Inspect').replace('ammo.','ammo->')
cover=body(p,'bool CoverAlive()').replace('foreach (','for (').replace('ERR_HunterTactics team','ERR_HunterTactics* team').replace('team.','team->')
run(r'''
#include <cassert>
#include <vector>
struct Man{bool fit=true;};bool CanFight(Man*m){return m&&m->fit;}
struct BaseCompartmentSlot{Man*occupant=nullptr;int type=1;int GetType(){return type;}Man*GetOccupant(){return occupant;}};
struct Members{Man*original;bool Contains(Man*m){return m==original;}};
struct ERR_HQMountedAmmoSnapshot{bool Complete=true;int Rounds=99,Chambers=1;}ammo;auto Inspect(int){return &ammo;}
struct UnitType{int Entity=0;struct Members Members;std::vector<BaseCompartmentSlot*>Seats;bool driver=true;bool CrewValid(int){return driver;}};
struct GunTeam{bool covered=true;bool Cover(){return covered;}};
struct ERR_HunterTactics{GunTeam*m_GunTeam=nullptr;UnitType*Unit;int Role=0,State=3,m_Mode=0;bool Delivered=true,living=true;bool Living(){return living;}
'''+armed+r'''
};struct Attack{std::vector<ERR_HunterTactics*>Teams;
'''+cover+r'''
};int main(){Man gunner,outsider;BaseCompartmentSlot slot;slot.occupant=&gunner;UnitType u;u.Members.original=&gunner;u.Seats={&slot};ERR_HunterTactics t;t.Unit=&u;Attack a;a.Teams={&t};assert(a.CoverAlive());gunner.fit=false;assert(!a.CoverAlive());gunner.fit=true;slot.occupant=&outsider;assert(!a.CoverAlive());slot.occupant=&gunner;u.driver=false;assert(!a.CoverAlive());u.driver=true;t.Delivered=false;assert(!a.CoverAlive());t.Delivered=true;t.living=false;assert(!a.CoverAlive());t.living=true;ammo.Rounds=0;ammo.Chambers=0;assert(!a.CoverAlive());ammo.Chambers=1;assert(a.CoverAlive());ammo.Complete=false;assert(!a.CoverAlive());GunTeam split;t.m_GunTeam=&split;t.Role=1;t.State=4;assert(a.CoverAlive());split.covered=false;assert(!a.CoverAlive());}
''')
print('PASS: covering support requires the original conscious turret gunner, valid mounted driver, live crew and delivered assignment')
# Execute production per-vehicle approach geometry: opposite arrivals stay on
# their own sides instead of sharing the cover vehicle's staging axis.
approach=body(p,'static vector Approach(').replace('vector ', 'Vec ')
run(stubs+approach+r'''
int main(){
Vec contact{100,0,100},fallback{0,0,1};
for(Vec origin : {Vec{100,0,-900},Vec{100,0,1100},Vec{-900,0,100},Vec{1100,0,100}}){
Vec forward=Approach(origin,contact,fallback);
Vec stage=contact-forward*260;
assert(Geometry::Dot(stage-contact,origin-contact)>0);
assert(Geometry::DistanceXZ(origin,stage)<Geometry::DistanceXZ(origin,contact));
assert(std::abs(forward.Length()-1)<0.0001);
}
assert(Approach(contact,contact,fallback).z==1);
}
''')
# Execute alternate-stage commit/rollback and reservations, not a duplicate planner.
alternate=body(p,'protected bool AlternateStage(').replace('protected ','').replace('array<vector>', 'Array<Vec>').replace('foreach (', 'for (').replace('vector ', 'Vec ').replace('ERR_HunterTactics other', 'Tactics* other')
for name in ['Unit','Attack','other']:
alternate=alternate.replace(name+'.',name+'->')
alternate=alternate.replace('Entity.GetOrigin()', 'Entity->GetOrigin()').replace('Math.Min', 'std::min').replace('ERR_HunterAttackRules.ManeuverBudget', 'ManeuverBudget').replace('m_ApproachStarted + 300000', 'm_ApproachStarted + 300000.f')
run(stubs.replace('template<class T>using Array=std::vector<T>;', 'template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;void Insert(T v){this->push_back(v);}};')+r'''
struct Entity{Vec point{0,0,-600};Vec GetOrigin(){return point;}};
struct UnitType{struct Entity entity;struct Entity*Entity=&entity;};
struct Tactics;struct AttackType{Array<Tactics*>Teams;};
float ManeuverBudget(float){return 240000;}
struct Tactics{
int rejected=0;void RejectStage(float){rejected++;}
UnitType unit;UnitType*Unit=&unit;AttackType attack;AttackType*Attack=&attack;
Vec Stage{0,0,-210},Approach{155,0,-240},Sector{105,0,35},m_ApproachPosition;
Array<Vec>m_RejectedStages,reservations;int State=3;float m_BlindSince=1000,m_ApproachProgress=0,m_ApproachStarted=1000,m_TravelDistance=1000,Deadline=100;
bool live=true,plan=true,move=true;bool Living(){return live;}
bool Plan(Array<Vec> occupied){reservations=occupied;Stage={200,0,-210};Approach={200,0,-85};Sector={105,0,35};return plan;}
bool Move(Vec,float){return move;}void Note(string){}
'''+alternate+r'''
};
int main(){
for(int failure=0;failure<2;failure++){
Tactics t;t.plan=failure!=0;t.move=failure!=1;
assert(!t.AlternateStage(21000));assert(t.Stage.x==0&&t.Approach.x==155&&t.State==3&&t.m_RejectedStages.empty()&&t.rejected==1);
}
Tactics t,other,dead;dead.live=false;other.Stage={-200,0,-210};t.attack.Teams={&t,&other,&dead};t.m_RejectedStages.Insert({-100,0,-300});
assert(t.AlternateStage(21000));assert(t.reservations.size()==4&&t.reservations.back().x==-200);
assert(t.State==1&&t.Stage.x==200&&t.m_BlindSince==-1&&t.m_ApproachProgress==21000&&t.m_RejectedStages.size()==2);
assert(t.Deadline==261000);assert(t.AlternateStage(200000)&&t.Deadline==301000); // retries cannot extend the total approach budget
}
''')
print('PASS: alternate positions exclude failed and occupied stages, roll back on failed planning/allocation, and preserve the total time budget')