Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
92 lines
5.5 KiB
Python
92 lines
5.5 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,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;a.Source="a";b.Source="b";c.Source="c";
|
|
bike.Source="bike";bike.Motorbike=true;s.m_Hunters={&a,&b,&c,&bike};
|
|
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')
|