82 lines
4.5 KiB
Python
82 lines
4.5 KiB
Python
"""Execute the actual bounded HQ report scheduler; radio integration stays native."""
|
|
from policy_harness import ROOT, body, run
|
|
from pathlib import Path
|
|
import os
|
|
source = ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQObservation.c'
|
|
hq = Path(os.environ.get('ERR_HQ_SOURCE',ROOT/'addons/ai/Scripts/Game/ERR/ERR_HQ.c'))
|
|
select = body(source, 'ERR_HQVisualContact Select(')
|
|
select = select.replace('array<ref ERR_HQVisualContact> samples', 'Array<ERR_HQVisualContact> samples')
|
|
select = select.replace('ERR_HQVisualContact selected;', 'ERR_HQVisualContact selected = nullptr;')
|
|
select = select.replace('foreach (ERR_HQVisualContact sample : samples)', 'for (ERR_HQVisualContact sample : samples)')
|
|
select = select.replace('sample.Entity', 'sample->Entity')
|
|
queued = body(source, 'void Queued(')
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <algorithm>
|
|
#include <vector>
|
|
struct Entity {int id;}; using IEntity=Entity*;
|
|
struct Sample {IEntity Entity;}; using ERR_HQVisualContact=Sample*;
|
|
template<class T> struct Array: std::vector<T> {
|
|
using std::vector<T>::vector;
|
|
int Count(){return this->size();}
|
|
int Find(T a){auto i=std::find(this->begin(),this->end(),a);return i==this->end()?-1:int(i-this->begin());}
|
|
void Remove(int i){this->erase(this->begin()+i);}
|
|
void Insert(T a){this->push_back(a);}
|
|
};
|
|
struct Selection {
|
|
Array<IEntity> m_Recent;
|
|
'''+select+queued+r'''
|
|
};
|
|
int main(){
|
|
Entity actors[80]{}; Sample samples[80]{};
|
|
for(int i=0;i<80;i++){actors[i].id=i;samples[i].Entity=&actors[i];}
|
|
Array<ERR_HQVisualContact> visible{&samples[0],&samples[1],&samples[2]};
|
|
Selection choice;
|
|
assert(!choice.Select({}));assert(!choice.Select({nullptr}));
|
|
Sample invalid{nullptr};assert(!choice.Select({&invalid}));
|
|
// Failed route allocation doesn't consume a turn: repeated Select is read only.
|
|
for(int i=0;i<5;i++)assert(choice.Select(visible)==&samples[0]);
|
|
assert(choice.m_Recent.empty());choice.Queued(nullptr);assert(choice.m_Recent.empty());
|
|
// Stable native order: every visible actor is reported before anyone repeats.
|
|
for(int i=0;i<30;i++){
|
|
auto selected=choice.Select(visible);assert(selected==&samples[i%3]);
|
|
choice.Queued(selected->Entity);assert(choice.m_Recent.Count()==3||i<2);
|
|
}
|
|
// Reordering native targets does not reset fairness. New contacts take priority.
|
|
assert(choice.Select({&samples[2],&samples[1],&samples[0]})==&samples[0]);
|
|
assert(choice.Select({&samples[0],&samples[3]})==&samples[3]);
|
|
choice.Queued(&actors[3]);
|
|
// Lost sight never reports a remembered actor; only current samples can win.
|
|
assert(choice.Select({&samples[2]})==&samples[2]);
|
|
// Interrupted delivery counts as an attempt, allowing another current contact.
|
|
choice.Queued(&actors[0]);assert(choice.Select(visible)==&samples[1]);
|
|
choice.Queued(&actors[1]);assert(choice.Select(visible)==&samples[2]);
|
|
// Repeated queue identity is deduplicated, and vanished references are purged.
|
|
choice.Queued(&actors[1]);assert(choice.m_Recent.Count()==4);
|
|
choice.m_Recent[0]=nullptr;choice.Queued(&actors[4]);
|
|
assert(choice.m_Recent.Find(nullptr)==-1);
|
|
// History and full 64-contact scheduling remain bounded under turnover.
|
|
Selection many;Array<ERR_HQVisualContact> sixtyFour;
|
|
for(int i=0;i<64;i++)sixtyFour.Insert(&samples[i]);
|
|
for(int i=0;i<192;i++){
|
|
auto selected=many.Select(sixtyFour);assert(selected==&samples[i%64]);
|
|
many.Queued(selected->Entity);assert(many.m_Recent.Count()<=64);
|
|
}
|
|
for(int i=64;i<80;i++)many.Queued(&actors[i]);
|
|
assert(many.m_Recent.Count()==64&&many.m_Recent.Find(&actors[0])==-1);
|
|
// Scheduling is per squad/round, never shared HQ intelligence.
|
|
Selection other;assert(other.Select(visible)==&samples[0]);
|
|
}
|
|
''')
|
|
observe=body(hq,'protected void Observe(')
|
|
assert 'samples[0]' not in observe
|
|
assert observe.index('ERR_HQObservation.Gather(')<observe.index('unit.ReportSelection.Select(samples)')
|
|
assert observe.index('unit.Uplink || now < unit.NextReport')<observe.index('unit.ReportSelection.Select(samples)')
|
|
assert observe.index('if (!route) return;')<observe.index('unit.PendingReport = report;')<observe.index('unit.Uplink = route;')<observe.index('unit.ReportSelection.Queued(seen.Entity)')
|
|
assert observe.count('unit.ReportSelection.Queued(')==1
|
|
assert 'Accept(' not in observe and 'AddWaypoint' not in observe
|
|
assert 'report.ObservedAt = seen.ObservedAt;' in observe
|
|
assert 'ERR_HQObservation.Describe(report, samples, now);' in observe
|
|
assert 'ref ERR_HQReportSelection ReportSelection = new ERR_HQReportSelection();' in hq.read_text()
|
|
print('PASS: actual per-squad bounded report selector rotates visible targets, survives ordering/turnover, records only queued attempts, and preserves radio intake ordering')
|