Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
116 lines
9.5 KiB
Python
116 lines
9.5 KiB
Python
"""Execute radio-site reinforcement selection, quotas and defence geometry from production."""
|
|
import re
|
|
from policy_harness import ROOT, body, run
|
|
P=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_BodyRecovery.c'
|
|
HQ=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'
|
|
def cpp(s):
|
|
s=s.replace('protected ', '').replace('foreach (', 'for (').replace('null', 'nullptr')
|
|
s=s.replace('array<string> sites', 'Array<string> sites').replace('array<vector> reserved', 'Array<Vec> reserved')
|
|
s=s.replace('array<ERR_LocationComponent> locations = {};', 'Array<ERR_LocationComponent*> locations;')
|
|
s=s.replace('array<ERR_HQUnit> candidates = {};', 'Array<ERR_HQUnit*> candidates;')
|
|
s=s.replace('out vector position', 'Vec& position').replace('vector ', 'Vec ')
|
|
for typ in ('ERR_HQUnit','ERR_HQTask','ERR_HQWatchEntry','ERR_LocationComponent','ERR_HQRoute','ERR_HQReport'):
|
|
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
|
|
for name in ('unit','target','location','planned','best','candidate','status','task','lineage','m_Mode'):
|
|
s=s.replace(name+'.',name+'->')
|
|
s=s.replace('PendingTask.', 'PendingTask->').replace('Task.', 'Task->')
|
|
for typ in ('ERR_BodyRecoveryRules','ERR_InfantryResupplyRules','ERR_LocationKind','ERR_HQPursuitRole','ERR_Actors','ERR_HQRoadIntercept','Math','vector'):
|
|
s=s.replace(typ+'.',('Geometry' if typ=='vector' else typ)+'::')
|
|
s=s.replace('ERR_HQ.Event','Event').replace('string.Format','Format').replace('GetGame().GetWorld().GetWorldTime()', 'clock')
|
|
s=s.replace('int committed;', 'int committed=0;').replace('int queued;', 'int queued=0;').replace('int count;', 'int count=0;').replace('int total;', 'int total=0;')
|
|
s=s.replace('bool occupied;', 'bool occupied=false;').replace('ERR_HQUnit* best;', 'ERR_HQUnit* best=nullptr;').replace('ERR_LocationComponent* target;', 'ERR_LocationComponent* target=nullptr;')
|
|
return s
|
|
rules=cpp(body(P,'class ERR_BodyRecoveryRules')).replace('class ERR_BodyRecoveryRules','struct ERR_BodyRecoveryRules')+';'
|
|
methods='\n'.join(cpp(body(P,s)) for s in ('protected bool SecurityCandidate(', 'protected bool SecurityPosition(', 'protected void TrySecureSites('))
|
|
budgets='\n'.join(cpp(body(HQ,s)) for s in ('protected bool SecurityAssigned(', 'protected int ResponseAssigned(', 'protected int HuntingAssigned('))
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <vector>
|
|
#include <string>
|
|
#include <map>
|
|
#include <cmath>
|
|
#include <algorithm>
|
|
struct string:std::string{using std::string::string;string(std::string s):std::string(s){}bool IsEmpty(){return empty();}};
|
|
template<class...T> string Format(string s,T...){return s;}void Event(string,string){}
|
|
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*(float f){return {x*f,y*f,z*f};}};
|
|
Vec Vector(float x,float y,float z){return {x,y,z};}
|
|
namespace Geometry{float DistanceXZ(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);}}
|
|
namespace Math{const float DEG2RAD=3.14159265359f/180;float Sin(float x){return std::sin(x);}float Cos(float x){return std::cos(x);}float Min(float a,float b){return std::min(a,b);}}
|
|
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;void Insert(T t){this->push_back(t);}int Count(){return this->size();}bool IsEmpty(){return this->empty();}void RemoveItem(T t){this->erase(std::find(this->begin(),this->end(),t));}};
|
|
namespace ERR_LocationKind{enum{RADIO=1,DEPOT=2};}
|
|
namespace ERR_HQPursuitRole{enum{SEARCH};}
|
|
namespace ERR_Actors{Vec Ground(Vec p){p.y=10;return p;}}
|
|
namespace ERR_HQRoadIntercept{bool standable=true;int calls=0;bool Standable(Vec){calls++;return standable;}}
|
|
struct ERR_HQTask{string SecuritySite;float ExpiresAt=0,QueuedAt=0;int Role=0;Vec Destination;bool ReleaseOnly=false,investigation=false;ERR_HQTask* ReplacesTask=nullptr;bool Investigation(){return investigation;}};
|
|
struct ERR_HQReport{Vec Position;float ObservedAt=0;bool Distress=false;};
|
|
struct ERR_HQRoute{};
|
|
struct ERR_HQWatchEntry{Vec Position;bool fresh=true,blocked=false;};
|
|
namespace ERR_InfantryResupplyRules{bool Fresh(ERR_HQWatchEntry*s,float){return s&&s->fresh;}}
|
|
struct Group{bool connected=true;};
|
|
struct ERR_HQUnit{int Id=0,ForceSourceId=0;bool HomeGuard=false,eligible=true,fighting=false,Radio=true,PendingCheckin=false,PendingConvoyOrder=false;
|
|
struct Group group;struct Group* Group=&group;ERR_HQTask* Task=nullptr;ERR_HQTask* PendingTask=nullptr;ERR_HQReport* Assignment=nullptr;ERR_HQReport* PendingOrder=nullptr;ERR_HQRoute* Downlink=nullptr;
|
|
bool Eligible(void*){return eligible;}ERR_HQTask* PlannedTask(){return PendingTask?PendingTask:Task;}};
|
|
struct Watch{std::map<int,ERR_HQWatchEntry> entries;ERR_HQWatchEntry* Find(int id){return &entries[id];}bool ArmedSearchBlocked(int id,float){return entries[id].blocked;}};
|
|
struct Network{bool connected=true;bool Route(bool,bool){return connected;}};
|
|
struct Compromised{std::map<string,float> times;float Get(string s){return times[s];}};
|
|
struct ERR_LocationComponent{string id="radio";int kind=ERR_LocationKind::RADIO;Vec position;string Id(){return id;}int Kind(){return kind;}Vec Position(){return position;}};
|
|
struct Mode{Array<ERR_LocationComponent*> sites;void Locations(Array<ERR_LocationComponent*>& out){out=sites;}};
|
|
'''+rules+r'''
|
|
struct HQ{float clock=1000;int queued=0,mounted=0;Mode mode;Mode* m_Mode=&mode;Watch m_Watch;Network m_Network;Compromised m_Compromised;Array<ERR_HQUnit*> m_Units;
|
|
bool Fighting(ERR_HQUnit*u){return u->fighting;}
|
|
ERR_HQRoute* Downlink(Group*g,string,string,int){if(!g->connected)return nullptr;queued++;return new ERR_HQRoute();}
|
|
int Assigned(){int total=mounted;for(auto* u:m_Units)if(u->Assignment||u->PendingOrder)total++;return total;}
|
|
'''+methods+budgets+r'''
|
|
};
|
|
struct Fixture{HQ h;ERR_LocationComponent radio;ERR_HQUnit units[6];Fixture(){h.mode.sites={&radio};h.m_Compromised.times["radio"]=1800000;
|
|
for(int i=0;i<6;i++){units[i].Id=i+1;units[i].ForceSourceId=i;h.m_Units.Insert(&units[i]);h.m_Watch.entries[i].Position={float((i+1)*100),0,0};}}
|
|
void dispatch(){h.TrySecureSites({"radio"});}
|
|
int count(){int n=0;for(auto&u:units)if(u.PendingTask)n++;return n;}};
|
|
int main(){
|
|
for(int n=0;n<10;n++)assert(ERR_BodyRecoveryRules::TeamSlot(n)==(n<4));
|
|
assert(ERR_BodyRecoveryRules::SpawnBudget(11,47,253));
|
|
assert(!ERR_BodyRecoveryRules::SpawnBudget(12,0,0)&&!ERR_BodyRecoveryRules::SpawnBudget(0,48,0)&&!ERR_BodyRecoveryRules::SpawnBudget(0,0,254));
|
|
assert(ERR_BodyRecoveryRules::SecurityTarget(true,0)==0);
|
|
for(int n=1;n<=128;n++){int k=ERR_BodyRecoveryRules::SecurityTarget(true,n);assert(k==(2*n+2)/3&&k<=n);assert(ERR_BodyRecoveryRules::SecurityTarget(false,n)==1);}
|
|
{Fixture f;f.dispatch();assert(f.count()==4&&f.h.queued==4); // two thirds, nearest first
|
|
for(int i=0;i<4;i++){auto*t=f.units[i].PendingTask;assert(t&&t->SecuritySite=="radio"&&t->ExpiresAt==601000&&!f.units[i].Task);
|
|
auto d=Geometry::DistanceXZ(t->Destination,{});assert(d>=119.9&&d<=220.1);
|
|
for(int j=0;j<i;j++)assert(Geometry::DistanceXZ(t->Destination,f.units[j].PendingTask->Destination)>=60);}
|
|
f.dispatch();assert(f.count()==4&&f.h.queued==4); // pending orders count: no growth toward 100% or duplication
|
|
f.h.mounted=2;assert(f.h.Assigned()==6&&f.h.ResponseAssigned()==2&&f.h.HuntingAssigned()==0); // preserve separately counted mounted dispatches
|
|
ERR_HQTask pursuit;ERR_HQReport report;f.units[4].Task=&pursuit;f.units[4].Assignment=&report;
|
|
assert(f.h.ResponseAssigned()==3&&f.h.HuntingAssigned()==1);
|
|
}
|
|
{Fixture f;f.radio.kind=ERR_LocationKind::DEPOT;f.dispatch();assert(f.count()==1&&f.units[0].PendingTask->Destination.x==0);}
|
|
{Fixture f;f.units[0].group.connected=false;f.dispatch();assert(f.count()==4&&!f.units[0].PendingTask&&f.units[4].PendingTask);}
|
|
{Fixture f;f.h.m_Network.connected=false;f.dispatch();assert(f.count()==0);}
|
|
{Fixture f;ERR_HQRoadIntercept::standable=false;int calls=ERR_HQRoadIntercept::calls;f.dispatch();assert(f.count()==0&&ERR_HQRoadIntercept::calls==calls+6*24);ERR_HQRoadIntercept::standable=true;}
|
|
{Fixture f;f.h.m_Compromised.times.clear();f.dispatch();assert(f.count()==0); // no delivered compromise record
|
|
f.h.m_Compromised.times["radio"]=1000;f.dispatch();assert(f.count()==0);
|
|
f.h.m_Compromised.times["radio"]=1500;f.dispatch();assert(f.units[0].PendingTask->ExpiresAt==1500);}
|
|
{Fixture f;f.h.m_Watch.entries[0].Position.x=2500;assert(f.h.SecurityCandidate(&f.units[0],{},1000));
|
|
f.h.m_Watch.entries[0].Position.x=2501;assert(!f.h.SecurityCandidate(&f.units[0],{},1000));}
|
|
for(int fault=0;fault<14;fault++){Fixture f;auto&u=f.units[0];ERR_HQTask task;ERR_HQReport report;ERR_HQRoute route;
|
|
if(fault==0)u.eligible=false;
|
|
if(fault==1)u.HomeGuard=true;
|
|
if(fault==2)u.fighting=true;
|
|
if(fault==3)u.Radio=false;
|
|
if(fault==4)u.Task=&task;
|
|
if(fault==5)u.PendingTask=&task;
|
|
if(fault==6)u.Assignment=&report;
|
|
if(fault==7)u.PendingOrder=&report;
|
|
if(fault==8)u.Downlink=&route;
|
|
if(fault==9)u.PendingConvoyOrder=true;
|
|
if(fault==10)u.PendingCheckin=true;
|
|
if(fault==11)f.h.m_Watch.entries[0].fresh=false;
|
|
if(fault==12)f.h.m_Watch.entries[0].blocked=true;
|
|
if(fault==13)f.h.m_Network.connected=false;
|
|
assert(!f.h.SecurityCandidate(&u,{},1000));}
|
|
{Fixture f;ERR_LocationComponent other;other.id="other";f.h.mode.sites.Insert(&other);f.h.m_Compromised.times["other"]=1800000;
|
|
f.h.TrySecureSites({"radio","other"});int first=0,second=0;for(auto&u:f.units){if(u.PendingTask->SecuritySite=="radio")first++;else second++;}
|
|
assert(first==4&&second==2); // a pending group cannot be taken by another compromised site
|
|
}
|
|
}
|
|
''')
|
|
print('PASS: four-team/twelve-wave bounds; two-thirds nearby radio defence; nearest selection, radio failures, expiry, perimeter separation, protected duties and independent response budgets')
|