Files
EscapistsReintroduced/tests/test_hq_task_reservations.py
T
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00

152 lines
11 KiB
Python

"""Actual HQ reservation collection and search decisions across interrupted retasks.
Native radio/waypoint execution is not simulated; LostOrder's production ownership
and the real coverage/bearing policies are executed against detached task doubles.
"""
from pathlib import Path
import os, re
from policy_harness import ROOT, body, run
P=Path(os.environ.get('ERR_HQ_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c'))
C=Path(os.environ.get('ERR_HQ_COVERAGE_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQCoverage.c'))
R=ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_HQPursuit.c'
def loop(text, signature):
start=text.index(signature); opening=text.index('{',start); end=opening+1; depth=1
while depth:
depth+=(text[end]=='{')-(text[end]=='}');end+=1
return text[start:end]
def cpp(s):
s=s.replace('ref ', '')
s=s.replace('protected ','').replace('null','nullptr').replace('.IsEmpty()', '.empty()')
for name in ('Math','ERR_HQPursuitRules','ERR_HQCoverageRules','vector'):
s=s.replace(name+'.',name+'::')
s=s.replace('out vector ', 'vector& ').replace('out int ', 'int& ')
s=s.replace('array<vector> reserved','Array<vector>* reserved')
for typ in ('ERR_HQUnit','ERR_HQTask','ERR_HQHuntWindow'):
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
s=re.sub(r'foreach \((ERR_HQUnit\*) (\w+) : m_Units\)',r'for (\1 \2 : m_Units)',s)
s=s.replace('foreach (vector position : reserved)','for (vector position : *reserved)')
for name in ('unit','other','task','planned','hunt','reserved'):
s=s.replace(name+'.',name+'->')
s=s.replace('PendingTask.', 'PendingTask->')
s=s.replace('int total;','int total=0;').replace('bool found;','bool found=false;')
return s.replace('other->Task.', 'other->Task->')
assign=body(P,'protected void Assign(')
collect=loop(assign[assign.index(' array<vector> reserved = {};'):], 'foreach (ERR_HQUnit unit : m_Units)')
retask=body(P,'protected void Retask(')
retask_collect=loop(retask,'foreach (ERR_HQUnit other : m_Units)')
helpers=''
if 'protected void ReserveTask(' in P.read_text():
helpers='\n'.join(body(P,s) for s in ('protected void ReserveTask(', 'protected void ReserveUnit('))
# All three planning paths must retain the same ownership rule.
assert 'ReserveUnit(existing, reserved, first, second)' in body(P,'protected void Investigate(')
methods='\n'.join(body(P,s) for s in ('protected int Assigned(', 'protected void FinishWatch(', 'protected void LostOrder('))
if 'protected float HuntCommitment(' in P.read_text(): methods=body(P,'protected float HuntCommitment(')+'\n'+methods
if 'protected void ReconsiderHunt(' in P.read_text(): methods=body(P,'protected void ReconsiderHunt(')+'\n'+methods
if 'protected void RememberCompletedHunt(' in P.read_text(): methods=body(P,'protected void RememberCompletedHunt(')+'\n'+methods
unit_methods='\n'.join(body(P,s) for s in ('void CancelPending(', 'ERR_HQTask PlannedTask('))
rules='\n'.join(body(R,s) for s in ('static bool Finite(', 'static bool PositionValid(', 'static bool BearingAllowed('))
coverage_rules='\n'.join(body(C,s) for s in ('static bool PositionValid(', 'static int Cell(', 'static int Key(', 'static bool Point('))
coverage='\n'.join(body(C,s) for s in ('protected bool Reserved(', 'bool Select('))
run(r'''
#include <cassert>
#include <cmath>
#include <algorithm>
#include <vector>
#include <string>
struct vector {float x=0,y=0,z=0;float& operator[](int i){return i==0?x:i==1?y:z;}
static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}};
vector Vector(float x,float y,float z){return {x,y,z};}
struct Math {static float Clamp(float x,float a,float b){return std::clamp(x,a,b);}
static float Floor(float x){return std::floor(x);} static float AbsFloat(float x){return std::abs(x);}
static float Min(float a,float b){return std::min(a,b);}static float Max(float a,float b){return std::max(a,b);}};
template<class T>struct Array:std::vector<T>{int Count(){return this->size();}void Insert(T t){this->push_back(t);}};
struct ERR_HQPursuitRules {
'''+cpp(rules)+r'''};
struct ERR_HQCoverageRules {
'''+cpp(coverage_rules)+r'''};
struct Coverage {bool Recent(vector,float){return false;}
'''+cpp(coverage)+r'''};
struct ERR_HQTask {int OperationId=0,ExecutionId=0;float ExpiresAt=120000;vector Destination;int ApproachBearing=0,BeliefId=1;bool ReleaseOnly=false;
std::string SecuritySite,WatchSourceId;int WatchMessageId=0,WatchRequestId=0;bool Investigation(){return !WatchSourceId.empty();}};
struct ERR_HQReport{};struct ERR_HQRoute{};
struct ERR_HQUnit {int Id=1;ERR_HQTask* Task=nullptr;ERR_HQTask* PendingTask=nullptr;
ERR_HQReport* Assignment=nullptr;ERR_HQReport* PendingOrder=nullptr;ERR_HQRoute* Downlink=nullptr;
void* PendingConvoyOrder=nullptr;bool PendingCheckin=false;void* Waypoint=nullptr;
'''+cpp(unit_methods)+r'''};
struct ERR_HQHuntWindow {float CompletedUntil=0;float NextHunt=900000;};
struct Watch {void FinishInvestigation(std::string,int,int,float){}};
struct HQ {struct Operations{void Release(int,int,float){}}m_Operations;void ReserveAttackSpace(ERR_HQUnit*,float){}std::vector<ERR_HQUnit*>m_Units;ERR_HQHuntWindow window;Watch m_Watch;
ERR_HQHuntWindow* Hunt(int){return &window;}
'''+cpp(helpers+methods)+r'''
void Collect(Array<vector>* reserved,int& first,int& second){float now=1000;
'''+cpp(collect)+r'''}
void CollectForRetask(ERR_HQUnit* unit,Array<vector>* reserved,int& first,int& second){
'''+cpp(retask_collect)+r'''}
};
int main(){
ERR_HQTask active,pending;active.Destination={250,0,250};pending.Destination={1250,0,250};
active.ApproachBearing=0;pending.ApproachBearing=1;
ERR_HQReport delivered,replacement;ERR_HQRoute radio;int waypoint=1;
ERR_HQUnit unit;unit.Task=&active;unit.Assignment=&delivered;unit.Waypoint=&waypoint;
unit.PendingTask=&pending;unit.PendingOrder=&replacement;unit.Downlink=&radio;
HQ hq;hq.m_Units={&unit};Coverage coverage;Array<vector> reserved;int first=-1,second=-1;vector selected;
hq.Collect(&reserved,first,second);
// The old search continues physically while replacement is only queued.
// Previously only pending's cell was reserved, so this wrongly returned true.
assert(!coverage.Select(active.Destination,25,active.Destination,1000,selected,&reserved));
assert(!coverage.Select(pending.Destination,25,pending.Destination,1000,selected,&reserved));
assert(reserved.Count()==2&&hq.Assigned()==1);
assert(ERR_HQPursuitRules::BearingAllowed(0,first,second));
assert(ERR_HQPursuitRules::BearingAllowed(1,first,second));
assert(!ERR_HQPursuitRules::BearingAllowed(2,first,second));
// A broken radio transaction frees only pending's cell/sector, not the old task.
hq.LostOrder(&unit,1000);assert(unit.Task==&active&&unit.Waypoint==&waypoint&&unit.Assignment==&delivered);
assert(!unit.PendingTask&&!unit.PendingOrder&&!unit.Downlink&&hq.Assigned()==1);
reserved.clear();first=second=-1;hq.Collect(&reserved,first,second);
assert(reserved.Count()==1&&!coverage.Select(active.Destination,25,active.Destination,1001,selected,&reserved));
assert(coverage.Select(pending.Destination,25,pending.Destination,1001,selected,&reserved));
// After committed replacement, only the new physical task remains reserved.
unit.Task=&pending;unit.Assignment=&replacement;reserved.clear();first=second=-1;hq.Collect(&reserved,first,second);
assert(reserved.Count()==1&&coverage.Select(active.Destination,25,active.Destination,1002,selected,&reserved));
assert(!coverage.Select(pending.Destination,25,pending.Destination,1002,selected,&reserved));
// Retasking excludes its own superseded destination but retains both peer states.
unit.Task=&active;unit.PendingTask=&pending;unit.PendingOrder=&replacement;unit.Downlink=&radio;
ERR_HQUnit candidate;hq.m_Units.push_back(&candidate);reserved.clear();first=second=-1;
hq.CollectForRetask(&candidate,&reserved,first,second);assert(reserved.Count()==2);
reserved.clear();first=second=-1;hq.CollectForRetask(&unit,&reserved,first,second);assert(reserved.empty());
// Retask cannot rotate into a third sector while its old approach still runs:
// peer=0, own delivered=1, proposed replacement=2. Own cell is replaceable.
ERR_HQTask ownTask;ownTask.Destination={2250,0,250};ownTask.ApproachBearing=1;
unit.PendingTask=nullptr;unit.PendingOrder=nullptr;unit.Downlink=nullptr;
candidate.Task=&ownTask;candidate.Assignment=&delivered;
reserved.clear();first=second=-1;hq.CollectForRetask(&candidate,&reserved,first,second);
assert(reserved.Count()==1&&!coverage.Select(active.Destination,25,active.Destination,1003,selected,&reserved));
assert(coverage.Select(ownTask.Destination,25,ownTask.Destination,1003,selected,&reserved));
assert(!ERR_HQPursuitRules::BearingAllowed(2,first,second));
assert(ERR_HQPursuitRules::BearingAllowed(0,first,second)&&ERR_HQPursuitRules::BearingAllowed(1,first,second));
assert(hq.Assigned()==2);
candidate.Task=nullptr;candidate.Assignment=nullptr;unit.PendingOrder=&replacement;
// A recall only removes the active order upon delivery; it reserves no new point.
ERR_HQTask recall;recall.ReleaseOnly=true;recall.ApproachBearing=2;unit.PendingTask=&recall;
reserved.clear();first=second=-1;hq.Collect(&reserved,first,second);assert(reserved.Count()==1&&first==0&&second==-1);
// Duplicate pointer ownership must not spend two entries for the same task.
unit.PendingTask=&active;reserved.clear();first=second=-1;hq.Collect(&reserved,first,second);assert(reserved.Count()==1);
// A site-security task has no approach sector but still occupies its cell.
active.ApproachBearing=-1;unit.PendingTask=nullptr;unit.PendingOrder=nullptr;
reserved.clear();first=second=-1;hq.Collect(&reserved,first,second);assert(reserved.Count()==1&&first==-1&&second==-1);
active.ApproachBearing=0;unit.PendingOrder=&replacement;
// Third active/pending sector fails conservatively; it cannot overwrite an earlier sector.
ERR_HQTask third;third.ApproachBearing=2;candidate.Task=&third;candidate.Assignment=&delivered;
unit.PendingTask=&pending;reserved.clear();first=second=-1;hq.Collect(&reserved,first,second);
for(int sector=0;sector<4;sector++)assert(!ERR_HQPursuitRules::BearingAllowed(sector,first,second));
// Maximum eight squads, two snapshots apiece: 16 bounded reservation entries,
// while the physical-force budget still counts exactly eight squads.
ERR_HQUnit units[8];ERR_HQTask tasks[16];hq.m_Units.clear();
for(int i=0;i<8;i++){tasks[i*2].Destination={float(10000+i*1000),0,10000};tasks[i*2+1].Destination={float(10500+i*1000),0,10000};
units[i].Task=&tasks[i*2];units[i].Assignment=&delivered;units[i].PendingTask=&tasks[i*2+1];units[i].PendingOrder=&replacement;hq.m_Units.push_back(&units[i]);}
reserved.clear();first=second=-1;hq.Collect(&reserved,first,second);assert(hq.Assigned()==8&&reserved.Count()==16);
assert(coverage.Select({250,0,250},25,{250,0,250},2000,selected,&reserved));
reserved.Insert({20000,0,20000});assert(!coverage.Select({250,0,250},25,{250,0,250},2000,selected,&reserved));
}
''')
print('PASS: actual HQ active/pending reservations, interruption, committed replacement, bounded coverage and unchanged force counts')