92 lines
5.6 KiB
Python
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')
|