Files
EscapistsReintroduced/tests/test_infantry_resupply_coordinator.py
T

135 lines
12 KiB
Python

"""Actual coordinator decisions/transitions with native boundaries doubled; no engine proof."""
import os,re,sys
from pathlib import Path
REPO=Path(os.environ.get('ERR_REPO','/home/jakub/devel/escapistsrereintroduced'))
sys.path.insert(0,str(REPO/'tests'))
from policy_harness import body,run
SOURCE=Path(os.environ.get('ERR_INFANTRY_RESUPPLY_SOURCE',REPO/'addons/ai/Scripts/Game/ERR/ERR_InfantryResupply.c'))
def method(signature):
s=body(SOURCE,signature).replace('protected ','').replace('null','nullptr')
s=s.replace('vector.DistanceXZ','Vec::DistanceXZ').replace('ERR_ForceRules.','ERR_ForceRules::').replace('ERR_InfantryResupplyRules.','ERR_InfantryResupplyRules::').replace('ERR_Actors.','ERR_Actors::')
s=s.replace(' + "3 0 0"',' + Vec{3,0,0}')
s=re.sub(r'string\.Format\("walk to reported stopped ammo truck at %1", m_Rendezvous\)', '"rendezvous"', s)
for kind in ['ERR_HQWatchEntry','Physics']:
s=re.sub(r'\b'+kind+r' (\w+)',kind+r'* \1',s)
# Enforce native entity/reference member access becomes C++ pointer member access.
for name in ['entry','recipient','donor','stopped','m_HQ','m_Move','m_Recipient','m_Donor','m_Truck','m_Order','physics']:
s=s.replace(name+'.',name+'->')
for name in ['AmmoState','Group','Combat','Entity']:
s=s.replace(name+'.',name+'->')
s=s.replace('Watch().','Watch()->').replace('GetVelocity().','GetVelocity().').replace('GetAngularVelocity().','GetAngularVelocity().')
return s
rules='\n'.join(method(x) for x in ['static bool Fresh','static bool Need','static bool Pair'])
methods='\n'.join(method(x) for x in ['protected void ClearWaypoints','protected void Finish','void Tick(float now)','void Stop()'])
code=r'''
#include <cassert>
#include <cmath>
#include <limits>
#include <string>
#include <vector>
#include <algorithm>
using string=std::string;
struct Vec {float x=0,y=0,z=0; float Length(){return std::sqrt(x*x+y*y+z*z);} static float DistanceXZ(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);} Vec operator+(Vec b){return {x+b.x,y+b.y,z+b.z};} string ToString(){return "position";}};
struct ERR_ForceRules {static bool Finite(float n){return std::isfinite(n);} static bool FiniteVector(Vec v){return Finite(v.x)&&Finite(v.y)&&Finite(v.z);}};
struct Ammo {bool Complete=true,valid=true;int Members=2,SmallArmsMuzzles=2,SmallArmsRounds=60,SmallArmsChambers=0;bool Valid(int){return valid;}};
struct ERR_HQWatchEntry {bool Closed=false;int Count=2,Fit=2,SourceId=1;float ObservedAt=0;Vec Position;Ammo* AmmoState=nullptr;};
struct ERR_InfantryResupplyRules {
RULES
};
struct Waypoint {int id;};
struct Group {std::vector<Waypoint*> points;void RemoveWaypoint(Waypoint* p){points.erase(std::remove(points.begin(),points.end(),p),points.end());}};
struct ERR_Actors {static inline int deleted=0;static void DeleteSpawned(Waypoint*){deleted++;}};
struct Physics {Vec linear,angular;Vec GetVelocity(){return linear;} Vec GetAngularVelocity(){return angular;}};
struct Entity {Physics* physics=nullptr;Physics* GetPhysics(){return physics;}};
struct Combat {bool Held=false;};
struct Truck {int SupplyToken=7,clears=0;bool SupplyHeld=false;struct Combat* Combat=nullptr;struct Entity* Entity=nullptr;void ClearOrder(){clears++;}};
struct Unit {struct Group* Group=nullptr;int ForceSourceId=1;float NextStatus=-1;};
struct Leaf {int stops=0;bool unresolved=true;void Stop(){stops++;}bool Unresolved(){return unresolved;}};
struct Route {int result=0,advances=0;int Advance(float,int){advances++;return result;}};
struct Watch {ERR_HQWatchEntry* entry=nullptr;ERR_HQWatchEntry* Find(int){return entry;}};
struct HQ {int token=7,releases=0,downlinks=0;struct Watch watch;Route* next=nullptr;int Journal(){return 0;}int LogisticsToken(Group*){return token;}void ReleaseLogistics(Group*,int owned){releases++;if(token==owned)token=0;}struct Watch* Watch(){return &watch;} Route* Downlink(Group*,string,string,int){downlinks++;return next;}};
struct Service {
HQ* m_HQ;Truck* m_Truck;Unit* m_Recipient;Unit* m_Donor;Leaf* m_Move=nullptr;Route* m_Order=nullptr;Entity* m_Receiving=nullptr;
Waypoint* m_MoveWaypoint=nullptr;Waypoint* m_HoldWaypoint=nullptr;Vec m_Rendezvous;int m_State=2,m_Token=7,m_ReportId=11;
float m_NextTick=0,m_Deadline=200000,m_HeldAt=-1;bool m_Stopped=false,valid=true,quiet=true,roster=true,waypoints=true;int audits=0,transfers=0,waypointCalls=0,stocks=0;
bool Current(){return !m_Stopped;}bool TruckValid(){return valid;}bool Quiet(Unit*){return quiet;}bool Roster(){return roster;}void Audit(string){audits++;}void Stock(float){stocks++;}bool Waypoints(){waypointCalls++;return waypoints;}void Transfer(float){transfers++;}
METHODS
};
struct Fixture {Physics physics;Entity entity{&physics};Combat combat;Truck truck;Group group,donorGroup;Unit recipient{&group},donor{&donorGroup};HQ hq;Route order,next;Service s;
Fixture(){truck.Combat=&combat;truck.Entity=&entity;s.m_HQ=&hq;s.m_Truck=&truck;s.m_Recipient=&recipient;s.m_Donor=&donor;s.m_Order=&order;hq.next=&next;}
};
int main(){
Ammo ammo;ERR_HQWatchEntry demand,donor;demand.AmmoState=&ammo;donor.SourceId=2;donor.Position={200,500,0};
assert(ERR_InfantryResupplyRules::Need(&demand,120000));assert(ERR_InfantryResupplyRules::Pair(&demand,&donor,120000));
assert(!ERR_InfantryResupplyRules::Fresh(&demand,120001));assert(!ERR_InfantryResupplyRules::Fresh(nullptr,0));
demand.ObservedAt=1;assert(!ERR_InfantryResupplyRules::Fresh(&demand,0));demand.ObservedAt=0;
ammo.SmallArmsChambers=1;assert(!ERR_InfantryResupplyRules::Need(&demand,0));ammo.SmallArmsChambers=0;
ammo.Complete=false;assert(!ERR_InfantryResupplyRules::Need(&demand,0));ammo.Complete=true;
ammo.Members=1;assert(!ERR_InfantryResupplyRules::Need(&demand,0));ammo.Members=2;
ammo.valid=false;assert(!ERR_InfantryResupplyRules::Need(&demand,0));ammo.valid=true;
donor.SourceId=1;assert(!ERR_InfantryResupplyRules::Pair(&demand,&donor,0));donor.SourceId=2;donor.Position.x=201;assert(!ERR_InfantryResupplyRules::Pair(&demand,&donor,0));
demand.Position.x=std::numeric_limits<float>::quiet_NaN();assert(!ERR_InfantryResupplyRules::Need(&demand,0));demand.Position.x=0;
{Fixture f;f.s.Tick(0);assert(f.s.m_State==2&&!f.truck.SupplyHeld&&f.truck.clears==0&&f.s.waypointCalls==0);f.order.result=1;f.s.Tick(1000);assert(f.s.m_State==3&&f.truck.SupplyHeld&&f.truck.clears==1&&f.s.m_Order==nullptr);f.physics.linear.x=1;f.s.Tick(2000);assert(f.s.m_HeldAt==-1&&f.donor.NextStatus==-1);f.physics.linear.x=0;f.s.Tick(3000);assert(f.s.m_HeldAt==3000&&f.donor.NextStatus==3000);ERR_HQWatchEntry report;report.Position={20,0,5};report.ObservedAt=2999;f.hq.watch.entry=&report;f.s.Tick(4000);assert(f.hq.downlinks==0);report.ObservedAt=3000;f.s.Tick(5000);assert(f.s.m_State==4&&f.hq.downlinks==1&&f.s.m_Rendezvous.x==23);f.s.Tick(6000);assert(f.s.m_State==4&&f.s.waypointCalls==0&&f.s.transfers==0);f.next.result=1;f.s.Tick(7000);assert(f.s.m_State==5&&f.s.waypointCalls==1&&f.s.transfers==1);}
{Fixture f;f.order.result=-1;f.s.Tick(0);assert(f.s.m_State==1&&f.hq.token==0&&f.truck.SupplyToken==0&&!f.truck.SupplyHeld&&f.truck.clears==0);}
{Fixture f;f.s.m_State=4;f.order.result=-1;f.s.Tick(0);assert(f.s.waypointCalls==0&&f.s.transfers==0&&f.hq.token==0);}
{Fixture f;f.hq.token=99;f.truck.SupplyToken=99;f.truck.SupplyHeld=true;f.s.Tick(0);assert(f.s.m_State==1&&f.hq.token==99&&f.truck.SupplyToken==99&&f.truck.SupplyHeld);}
{Fixture f;Waypoint own1{1},own2{2},other{3};f.group.points={&own1,&other,&own2};f.s.m_MoveWaypoint=&own1;f.s.m_HoldWaypoint=&own2;Leaf leaf;f.s.m_Move=&leaf;f.truck.SupplyHeld=true;f.s.Stop();assert(f.group.points.size()==1&&f.group.points[0]==&other&&leaf.stops==1&&f.s.m_Move==&leaf&&f.hq.token==0&&!f.truck.SupplyHeld&&f.s.m_Stopped);int audits=f.s.audits;f.s.Stop();f.s.Tick(5000);assert(f.s.audits==audits&&leaf.stops==1);}
{Fixture f;Leaf leaf;f.s.m_Move=&leaf;f.s.Finish("timeout");f.s.Tick(0);assert(f.s.m_Move==&leaf);leaf.unresolved=false;f.s.Tick(1000);assert(f.s.m_Move==nullptr);}
{Fixture f;f.s.m_State=3;f.physics.linear.x=std::numeric_limits<float>::quiet_NaN();f.s.Tick(0);assert(f.s.m_HeldAt==-1&&f.donor.NextStatus==-1);}
{Fixture f;f.s.m_State=3;f.physics.angular.x=std::numeric_limits<float>::quiet_NaN();f.s.Tick(0);assert(f.s.m_HeldAt==-1&&f.donor.NextStatus==-1);}
{Fixture f;f.s.m_Deadline=0;f.s.Tick(0);assert(f.s.m_State==1&&f.order.advances==0&&f.hq.token==0);}
// Waiting for physical transfer never extends the original bounded rendezvous deadline.
{Fixture f;f.s.m_State=5;f.s.m_Deadline=1000;f.s.Tick(0);assert(f.s.transfers==1&&f.s.m_State==5);
f.s.Tick(1000);assert(f.s.transfers==1&&f.s.m_State==1&&f.hq.token==0&&!f.truck.SupplyHeld);}
{Fixture f;f.s.quiet=false;f.order.result=1;f.s.Tick(0);assert(f.s.m_State==1&&f.order.advances==0&&!f.truck.SupplyHeld);}
{Fixture f;f.s.m_State=4;f.order.result=1;f.s.waypoints=false;f.s.Tick(0);assert(f.s.m_State==1&&f.s.waypointCalls==1&&f.s.transfers==0&&f.hq.token==0);}
{Fixture f;f.s.m_State=3;f.s.m_HeldAt=100;ERR_HQWatchEntry report;report.ObservedAt=100;f.hq.watch.entry=&report;f.hq.next=nullptr;f.s.Tick(1000);assert(f.s.m_State==1&&f.hq.downlinks==1&&f.s.waypointCalls==0&&f.hq.token==0);}
}
'''.replace('RULES',rules).replace('METHODS',methods)
run(code)
print('infantry resupply actual rules, radio transitions, owned cleanup and unresolved-operation tests passed')
# Recheck permanent initial-roster eligibility during an active trip.
src=SOURCE
s=body(src,'protected bool Roster()').replace('protected ','')
s=s.replace('array<AIAgent> agents = {};','Array<Agent*> agents;')
s=s.replace('foreach (AIAgent agent : agents)','for (Agent* agent : agents)').replace('foreach (IEntity actor : m_Members)','for (Actor* actor : m_Members)')
for x in ['m_Recipient','agent']:s=s.replace(x+'.',x+'->')
s=s.replace('StatusSampler.','StatusSampler->').replace('Group.','Group->')
for x in ['ERR_Actors','SCR_FactionManager','CompartmentAccessComponent']:s=s.replace(x+'.',x+'::')
s=s.replace('m_Mode.','m_Mode->').replace('bool living;', 'bool living = false;')
run(r'''
#include <cassert>
#include <vector>
#include <algorithm>
template<class T> struct Array:std::vector<T>{using std::vector<T>::vector;bool IsEmpty(){return this->empty();}int Count(){return this->size();}bool Contains(T a){return std::find(this->begin(),this->end(),a)!=this->end();}};
struct Group;
struct Actor{bool alive=true,player=false,fight=true;Group* group=nullptr;int faction=1;};
struct Agent{Actor* actor;Actor* GetControlledEntity(){return actor;}};
struct Group{Array<Agent*> agents;void GetAgents(Array<Agent*>& out){out=agents;}};
struct Sampler{Group* group;bool released=false;int strength=1;Array<Actor*> original;int InitialStrength(){return strength;}Actor* InitialMember(int slot){return slot>=0&&slot<(int)original.size()?original[slot]:nullptr;}bool OwnsRoster(Group* g){return g==group;}bool CanAttribute(int,Actor*){return !released;}};
struct Unit{struct Group* Group;Sampler* StatusSampler;};
struct Mode{int EnemyFaction(){return 1;}};
struct ERR_Actors{static bool Alive(Actor* a){return a->alive;}static bool Player(Actor* a){return a->player;}static struct Group* Group(Actor* a){return a->group;}static bool CanFight(Actor* a){return a->fight;}};
struct SCR_FactionManager{static int SGetFaction(Actor* a){return a->faction;}};
struct CompartmentAccessComponent{static bool GetVehicleIn(Actor*){return false;}};
struct Service{Unit* m_Recipient;Mode* m_Mode;Array<Actor*> m_Members;bool Quiet(Unit*){return true;}
METHOD
};
int main(){Group group;Actor soldier;soldier.group=&group;Agent agent{&soldier};group.agents={&agent};Sampler sampler{&group,false,1,{&soldier}};Unit unit{&group,&sampler};Mode mode;Service service{&unit,&mode,{&soldier}};assert(service.Roster());sampler.released=true;assert(!sampler.CanAttribute(0,&soldier));
// Permanent sampler release survives returning to the physical group.
assert(!service.Roster());sampler.released=false;assert(service.Roster());
sampler.strength=33;assert(!service.Roster());sampler.strength=0;assert(!service.Roster());sampler.strength=1;
Actor foreign;foreign.group=&group;sampler.original={&foreign};assert(!service.Roster());
// Initial roster order, not current GetAgents order, determines the slot.
sampler.strength=2;sampler.original={&foreign,&soldier};assert(service.Roster());
soldier.player=true;assert(!service.Roster());soldier.player=false;
soldier.group=nullptr;assert(!service.Roster());soldier.group=&group;
soldier.alive=false;assert(!service.Roster());soldier.alive=true;
Agent extra{&foreign};group.agents.push_back(&extra);assert(!service.Roster());
}
'''.replace('METHOD',s))
print('PASS: actual resupply Roster preserves permanent release, original slot mapping and bounded ownership')