Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
"""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')
|
||||
Reference in New Issue
Block a user