Files

92 lines
5.6 KiB
Python

"""Execute reserve decisions and commits from production, with detached HQ reports."""
import re
from policy_harness import ROOT, body, run
p = ROOT / 'addons/core/Scripts/Game/ERR/10_AI/ERR_HunterService.c'
def translate(s):
s = s.replace('protected ', '').replace('foreach (', 'for (').replace('null', 'nullptr')
for typ in ('ERR_HQWatchEntry', 'ERR_HunterVehicle', 'ERR_HQ'):
s = re.sub(r'\b' + typ + r' (?=\w)', typ + '* ', s)
for name in ('entry', 'unit', 'hq', 'm_PendingReserve'):
s = s.replace(name + '.', name + '->')
return (s.replace('Watch().', 'Watch()->').replace('int count;', 'int count=0;')
.replace('ERR_HunterFleetRules.', 'ERR_HunterFleetRules::')
.replace('ERR_HQ.Event', 'ERR_HQ::Event').replace('string.Format', 'Format'))
rules = '\n'.join(translate(body(p, sig)) for sig in ('static bool Unavailable(', 'static bool ReserveDue('))
methods = '\n'.join(translate(body(p, sig)) for sig in (
'int Deployed(', 'protected int AvailableToHQ(', 'protected ERR_HunterVehicle ReserveSource(',
'protected void CommitReserve('))
run(r'''
#include <cassert>
#include <string>
#include <vector>
#include <map>
using string=std::string;
template<class... T>string Format(string s,T...){return s;}
struct ERR_HQWatchEntry{bool Closed=false;int Fit=3;float ObservedAt=-1;};
struct Watch{std::map<string,ERR_HQWatchEntry*> entries;ERR_HQWatchEntry* Find(string s){return entries[s];}};
struct ERR_HQ{struct Watch watch;struct Watch* Watch(){return &watch;}static void Event(string,string){}};
struct ERR_HunterVehicle{bool Motorbike=false,Sarka=false,ReserveCovered=false,Released=false;float LaunchedAt=0;string Source;};
struct ERR_HunterFleetRules{
'''+rules+r'''
};
struct Service{std::vector<ERR_HunterVehicle*> m_Hunters;int m_Target=3,m_ReserveSpent=0;
float m_NextReserve=0;ERR_HunterVehicle* m_PendingReserve=nullptr;
'''+methods+r'''
};
int main(){
ERR_HQWatchEntry report;
assert(!ERR_HunterFleetRules::Unavailable(nullptr,100,480099));
assert(ERR_HunterFleetRules::Unavailable(nullptr,100,480100));
report.ObservedAt=100000;assert(!ERR_HunterFleetRules::Unavailable(&report,100,579999));
assert(ERR_HunterFleetRules::Unavailable(&report,100,580000));
report.Fit=0;assert(ERR_HunterFleetRules::Unavailable(&report,100,101));
report.Fit=3;report.Closed=true;assert(ERR_HunterFleetRules::Unavailable(&report,100,101));
assert(!ERR_HunterFleetRules::Unavailable(&report,100,99));
assert(!ERR_HunterFleetRules::ReserveDue(3,3,0,2,0,480000));
assert(!ERR_HunterFleetRules::ReserveDue(3,4,0,0,0,480000)); // aggression deployment first
assert(!ERR_HunterFleetRules::ReserveDue(15,12,3,0,0,480000));
assert(!ERR_HunterFleetRules::ReserveDue(15,15,0,0,0,480000));
assert(!ERR_HunterFleetRules::ReserveDue(6,3,3,0,0,480000));
assert(!ERR_HunterFleetRules::ReserveDue(4,3,1,0,960000,959999));
assert(ERR_HunterFleetRules::ReserveDue(4,3,1,1,960000,960000));
Service s;ERR_HQ hq;ERR_HunterVehicle a,b,c,bike,sarka;a.Source="a";b.Source="b";c.Source="c";
bike.Source="bike";bike.Motorbike=true;sarka.Sarka=true;sarka.Source="sarka";s.m_Hunters={&sarka,&a,&b,&c,&bike};assert(s.Deployed()==3);
a.Released=true;assert(s.AvailableToHQ(&hq,100)==3); // physical loss alone reveals nothing
assert(!s.ReserveSource(&hq,479999));
assert(s.ReserveSource(&hq,480000)==&a); // absence of a first check-in has a bounded deadline
report.Closed=false;report.ObservedAt=480000;hq.watch.entries["a"]=&report;hq.watch.entries["b"]=&report;
assert(!s.ReserveSource(&hq,480000)); // fresh returning radios avert replacement
report.Fit=0;assert(s.ReserveSource(&hq,480000)==&a); // delivered unfit crew can unlock reserve
hq.watch.entries.clear();
s.m_PendingReserve=s.ReserveSource(&hq,480000);
assert(s.m_ReserveSpent==0&&!a.ReserveCovered); // choosing/placement deferral does not spend
ERR_HunterVehicle replacement;replacement.Source="replacement";replacement.LaunchedAt=480000;
s.m_Hunters.push_back(&replacement);s.CommitReserve(&replacement,480000);
assert(s.m_ReserveSpent==1&&a.ReserveCovered&&s.m_NextReserve==960000&&!s.m_PendingReserve);
s.CommitReserve(&replacement,480001);assert(s.m_ReserveSpent==1); // one commit per departure attempt
assert(!s.ReserveSource(&hq,959999));assert(s.ReserveSource(&hq,960000)==&b);
b.ReserveCovered=true;c.ReserveCovered=true;replacement.ReserveCovered=true;
assert(!s.ReserveSource(&hq,960000)); // no re-use of a covered loss, bikes cannot unlock reserves
// A prolonged radio blackout admits exactly three reserves, eight minutes apart, then stops.
Service outage;ERR_HQ silent;ERR_HunterVehicle originals[3],reserves[3];
for(int i=0;i<3;i++){originals[i].Source="original"+std::to_string(i);outage.m_Hunters.push_back(&originals[i]);}
for(int i=0;i<3;i++){
float now=(i+1)*480000;assert(!outage.ReserveSource(&silent,now-1));
outage.m_PendingReserve=outage.ReserveSource(&silent,now);assert(outage.m_PendingReserve);
reserves[i].Source="reserve"+std::to_string(i);reserves[i].LaunchedAt=now;
outage.m_Hunters.push_back(&reserves[i]);outage.CommitReserve(&reserves[i],now);
}
assert(outage.Deployed()==6&&!outage.ReserveSource(&silent,9999999));
outage.m_Target=4;assert(!outage.ReserveSource(&silent,9999999)); // next ordinary escalation remains separate
}
''')
spawn = body(p, 'protected bool SpawnHunter(')
assert spawn.index('ERR_EnemyLoadoutCatalog.Available') < spawn.index('CommitReserve(unit, now)')
assert spawn.index('m_Hunters.Insert(unit)') < spawn.index('CommitReserve(unit, now)') < spawn.index('unit.Group =')
print('PASS: reserve uses HQ reports/dispatch deadlines, preserves radio uncertainty, spends once, respects cooldown/availability/three-reserve/15-vehicle caps')