Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
145 lines
11 KiB
Diff
145 lines
11 KiB
Diff
--- a/addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c
|
|
+++ b/addons/core/Scripts/Game/ERR/10_AI/ERR_HQ.c
|
|
@@ -1408,6 +1408,8 @@
|
|
Retask(now);
|
|
Investigate(now, globalLimit);
|
|
assigned = HuntingAssigned();
|
|
+ // Retask/investigation and cleanup above have settled commitments for this pass.
|
|
+ int totalAssigned = Assigned();
|
|
array<vector> reserved = {};
|
|
int first = -1;
|
|
int second = -1;
|
|
@@ -1417,7 +1419,7 @@
|
|
}
|
|
// Strongest delivered track first; finite scans (64 beliefs,128 own units).
|
|
array<int> considered = {};
|
|
- for (int pass = 0; pass < m_PlanBeliefs.Count() && assigned < limit && Assigned() < globalLimit; pass++)
|
|
+ for (int pass = 0; pass < m_PlanBeliefs.Count() && assigned < limit && totalAssigned < globalLimit; pass++)
|
|
{
|
|
ERR_HQBelief belief;
|
|
for (int i = 0; i < m_PlanBeliefs.Count(); i++)
|
|
@@ -1433,7 +1435,7 @@
|
|
if (belief.Kind == ERR_HQMemoryKind.ALARM && AlarmCommitted()) continue;
|
|
ERR_HQHuntWindow hunt = Hunt(belief.BeliefId);
|
|
if (!hunt || now < hunt.NextHunt) continue;
|
|
- for (int slot = 0; slot < limit && assigned < limit && Assigned() < globalLimit; slot++)
|
|
+ for (int slot = 0; slot < limit && assigned < limit && totalAssigned < globalLimit; slot++)
|
|
{
|
|
// One mobile search squad for uncertain sound; never empty objectives for it.
|
|
if ((belief.Kind == ERR_HQMemoryKind.GUNFIRE || belief.Kind == ERR_HQMemoryKind.ALARM) && slot > 0) break;
|
|
@@ -1489,6 +1491,7 @@
|
|
if (best.HomeGuard) m_HomeAvailable.Set(best.HomeId, HomeAvailable(best) - 1);
|
|
hunt.NextHunt = Math.Max(hunt.NextHunt, task.ExpiresAt + 180000);
|
|
assigned++;
|
|
+ totalAssigned++;
|
|
}
|
|
}
|
|
}
|
|
--- /dev/null
|
|
+++ b/tests/test_hq_planner_budget.py
|
|
@@ -0,0 +1,104 @@
|
|
+"""Actual planner selection/commit body: identical orders with one local count scan."""
|
|
+import os,re
|
|
+from pathlib import Path
|
|
+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'))
|
|
+s=body(P,'protected void Assign(')
|
|
+# Execute the complete final selection/commit loop. The preceding cleanup,
|
|
+# Retask and Investigate boundaries are represented by pre-existing commitments.
|
|
+start=s.index(' assigned = HuntingAssigned();')
|
|
+code=s[start:s.rfind('}')]
|
|
+# In the reference variant, restore only the old repeated-count loop guards.
|
|
+legacy=code.replace(' int totalAssigned = Assigned();','').replace('totalAssigned < globalLimit','Assigned() < globalLimit').replace(' totalAssigned++;','')
|
|
+assigned=body(P,'protected int Assigned(').replace('protected ','').replace('int Assigned()', 'int ActualAssigned()')
|
|
+def adapt(s):
|
|
+ s=s.replace('array<vector>','Arr<Vec>').replace('array<int>','Arr<int>').replace('vector destination','Vec destination').replace('vector position','Vec position')
|
|
+ s=s.replace('foreach (ERR_HQUnit unit : m_Units)','for (ERR_HQUnit* unit : m_Units)')
|
|
+ for cls in ('ERR_HQBelief','ERR_HQHuntWindow','ERR_HQUnit','ERR_HQRoute','ERR_HQTask'):
|
|
+ s=re.sub(r'\b'+cls+r' (\w+)',cls+r'* \1',s)
|
|
+ s=re.sub(r'(ERR_HQ(?:Belief|Unit|Route)\* \w+);',r'\1=nullptr;',s)
|
|
+ s=s.replace('int total;', 'int total=0;')
|
|
+ s=s.replace('bool roadIntercept;', 'bool roadIntercept=false;').replace('int bestId;', 'int bestId=0;')
|
|
+ for name in ('unit','belief','candidate','hunt','best','bestRoute','task'):
|
|
+ s=s.replace(name+'.',name+'->')
|
|
+ s=s.replace('unit->Group.GetOrigin()', 'unit->Group->GetOrigin()').replace('belief->Report.Snapshot()', 'belief->Report->Snapshot()')
|
|
+ s=s.replace('new ERR_HQTask()', 'NewTask()').replace('null','nullptr').replace('nullptrptr','nullptr')
|
|
+ for name in ('ERR_HQPursuitRules','ERR_HQPursuitRole','ERR_HQMemoryKind','ERR_HQRoadIntercept','Math'):
|
|
+ s=s.replace(name+'.',name+'::')
|
|
+ s=s.replace('vector.DistanceXZ','Distance').replace('string.Format','Format').replace('ERR_HQPursuitRole role','int role')
|
|
+ return s
|
|
+run(r'''
|
|
+#include <cassert>
|
|
+#include <string>
|
|
+#include <vector>
|
|
+#include <map>
|
|
+#include <algorithm>
|
|
+#include <cmath>
|
|
+#include <memory>
|
|
+using string=std::string;
|
|
+template<class T> struct Arr:std::vector<T>{using std::vector<T>::vector;int Count()const{return this->size();}void Insert(T t){this->push_back(t);}bool Contains(T t){return std::find(this->begin(),this->end(),t)!=this->end();}};
|
|
+struct Vec{float x=0;bool operator==(Vec b)const{return x==b.x;}};
|
|
+float Distance(Vec a,Vec b){return std::abs(a.x-b.x);}
|
|
+struct Math{static float Max(float a,float b){return std::max(a,b);}};
|
|
+template<class...T>string Format(string s,T...){return s;}
|
|
+namespace ERR_HQMemoryKind{enum{SIGHTING,GUNFIRE,ALARM};}
|
|
+namespace ERR_HQPursuitRole{enum{SEARCH,INTERCEPT,CONTAIN};}
|
|
+struct ERR_HQReport{int id=0;ERR_HQReport* Snapshot(){return this;}};
|
|
+struct ERR_HQTask{int BeliefId=0,BeliefRevision=0,Role=0,ApproachBearing=0;bool RoadIntercept=false;Vec Destination;float QueuedAt=0,ExpiresAt=0;string Name(){return "task";}};
|
|
+struct ERR_HQRoute{int MessageId=0;string Payload;};
|
|
+struct Group{Vec pos;Vec GetOrigin(){return pos;}};
|
|
+struct ERR_HQUnit{int Id=0,Radio=0,HomeId=0,ForceSourceId=0;struct Group* Group=nullptr;ERR_HQReport* Assignment=nullptr;ERR_HQReport* PendingOrder=nullptr;ERR_HQTask* PendingTask=nullptr;ERR_HQRoute* Downlink=nullptr;bool PlanFighting=false,PlanEligible=true,HomeGuard=false;Vec PlanPosition;};
|
|
+struct ERR_HQBelief{int BeliefId=0,BeliefRevision=1,MessageId=1,Kind=0;float Confidence=.7,ObservedAt=1000,Radius=100;Vec Position,PredictedPosition,Velocity;bool HasVelocity=true;ERR_HQReport* Report=nullptr;};
|
|
+struct ERR_HQHuntWindow{float NextHunt=0;};
|
|
+struct ERR_HQPursuitRules{
|
|
+ static bool Actionable(float c,float age,float r){return c>=.15&&age>=0&&r<=2000;}
|
|
+ static bool Heading(Vec,bool h){return h;}
|
|
+ static int Role(float,float,float,bool h,int slot){return h?slot:0;}
|
|
+ static Vec Objective(Vec p,Vec,bool,float,float,float,int role,int slot){return {p.x+role*10+slot};}
|
|
+ static bool CanCommit(bool group,bool eligible,bool busy,bool guard,int available,int assigned,int limit){return group&&eligible&&!busy&&(!guard||available>1)&&assigned<limit;}
|
|
+ static int Bearing(Vec p,Vec){return ((int)p.x)%2;}
|
|
+ static bool BearingAllowed(int b,int a,int c){return a<0||b==a||c<0||b==c;}
|
|
+ static float Score(int,Vec,Vec,Vec p,Vec d){return Distance(p,d);}
|
|
+ static bool Better(float score,int id,float old,int oldid){return old<0||score<old||(score==old&&id<oldid);}
|
|
+ static float TaskLifetimeSeconds(int,float distance){return 20+distance;}
|
|
+};
|
|
+struct ERR_HQRoadIntercept{static bool Select(ERR_HQBelief* b,float,Arr<Vec>& r,Vec& p){p.x=b->PredictedPosition.x+100+r.Count();return b->BeliefId%2==0;}};
|
|
+struct Watch{bool ArmedSearchBlocked(int source,float){return source%11==0;}};
|
|
+struct Coverage{bool allow=true;Arr<Vec> selections;bool Select(Vec p,float,Vec,float,Vec& result,Arr<Vec>& reserved){if(!allow)return false;result.x=p.x+reserved.Count()*25;selections.Insert(result);return true;}};
|
|
+struct Network{ERR_HQRoute routes[129];ERR_HQRoute* Route(int radio,bool){return radio%7?&routes[radio]:nullptr;}};
|
|
+struct Journal{int writes=0,seq=0;int Message(){return ++seq;}void Write(string,string,int){writes++;}};
|
|
+struct Homes{std::map<int,int> v;void Set(int id,int n){v[id]=n;}int Get(int id){return v[id];}};
|
|
+struct Harness {
|
|
+ Arr<ERR_HQUnit*> m_Units;Arr<ERR_HQBelief*> m_PlanBeliefs;Watch m_Watch;Coverage m_Coverage;Network m_Network;Journal m_Journal;Homes m_HomeAvailable;
|
|
+ std::map<int,ERR_HQHuntWindow> hunts;std::vector<std::unique_ptr<ERR_HQTask>> tasks;
|
|
+ int calls=0,visits=0,hunting=0;ERR_HQTask* NewTask(){tasks.push_back(std::make_unique<ERR_HQTask>());return tasks.back().get();}
|
|
+ COUNT
|
|
+ int Assigned(){calls++;visits+=m_Units.Count();return ActualAssigned();}
|
|
+ int HuntingAssigned(){return hunting;}
|
|
+ int HomeAvailable(ERR_HQUnit* u){return m_HomeAvailable.Get(u->HomeId);}
|
|
+ ERR_HQHuntWindow* Hunt(int id){return &hunts[id];}
|
|
+ bool AlarmCommitted(){for(auto u:m_Units)if(u->PendingTask&&u->PendingTask->BeliefId==63)return true;return false;}
|
|
+ Vec Containment(ERR_HQBelief*,Vec v){return v;}
|
|
+ void ReserveUnit(ERR_HQUnit* u,Arr<Vec>& reserved,int& first,int& second){if(!u->Assignment&&!u->PendingOrder)return;reserved.Insert(u->PlanPosition);int b=u->Id%2;if(first<0)first=b;else if(first!=b&&second<0)second=b;}
|
|
+ void Current(int globalLimit,int limit,float now){int assigned=0,radius=10000,roadQueries=0; CURRENT }
|
|
+ void Legacy(int globalLimit,int limit,float now){int assigned=0,radius=10000,roadQueries=0; LEGACY }
|
|
+};
|
|
+struct Setup{Harness h;ERR_HQUnit units[128];struct Group groups[128];ERR_HQReport reports[64];ERR_HQBelief beliefs[64];
|
|
+ Setup(int busy,int variant){
|
|
+ for(int i=0;i<128;i++){auto&u=units[i];u.Id=i+1;u.Radio=i+1;u.ForceSourceId=i+1;u.Group=&groups[i];u.PlanPosition={float(i+1)};u.HomeGuard=(i%5==0);u.HomeId=i%3;h.m_HomeAvailable.Set(u.HomeId,20);u.PlanEligible=variant!=1;u.PlanFighting=(i%17==0);if(i<busy){if(i%3!=1)u.Assignment=&reports[0];if(i%3!=0)u.PendingOrder=&reports[0];}h.m_Units.Insert(&u);}
|
|
+ for(int i=0;i<64;i++){auto&b=beliefs[i];b.BeliefId=i+1;b.Report=&reports[i];b.PredictedPosition={float(i*10)};b.Position=b.PredictedPosition;b.Confidence=.2f+.01f*i;b.Kind=i==62?ERR_HQMemoryKind::ALARM:(i%5==0?ERR_HQMemoryKind::GUNFIRE:0);h.m_PlanBeliefs.Insert(&b);h.hunts[i+1].NextHunt=(variant==2?100000:0);}
|
|
+ h.m_Coverage.allow=variant!=3;
|
|
+ }
|
|
+};
|
|
+void Equal(Setup&a,Setup&b){assert(a.h.m_Journal.writes==b.h.m_Journal.writes&&a.h.m_Coverage.selections==b.h.m_Coverage.selections&&a.h.m_HomeAvailable.v==b.h.m_HomeAvailable.v);
|
|
+ for(int i=0;i<128;i++){auto x=a.units[i].PendingTask,y=b.units[i].PendingTask;assert(bool(x)==bool(y));if(!x)continue;assert(x->BeliefId==y->BeliefId&&x->Role==y->Role&&x->RoadIntercept==y->RoadIntercept&&x->Destination==y->Destination&&x->ExpiresAt==y->ExpiresAt&&x->ApproachBearing==y->ApproachBearing);}
|
|
+ for(auto [id,window]:a.h.hunts)assert(window.NextHunt==b.h.hunts[id].NextHunt);
|
|
+}
|
|
+int main(){
|
|
+ for(int variant=0;variant<4;variant++)for(int global:{0,1,2,3,8,16})for(int busy:{0,1,2,5,16})for(int hunting:{0,1,3}){
|
|
+ Setup current(busy,variant),legacy(busy,variant);current.h.hunting=legacy.h.hunting=std::min(busy,hunting);current.h.Current(global,std::min(global,3),1000);legacy.h.Legacy(global,std::min(global,3),1000);Equal(current,legacy);assert(current.h.calls<=1);assert(current.h.ActualAssigned()<=std::max(busy,global));}
|
|
+ Setup current(0,1),legacy(0,1);current.h.Current(16,3,1000);legacy.h.Legacy(16,3,1000);Equal(current,legacy);assert(current.h.visits==128);assert(legacy.h.visits>10000);
|
|
+}
|
|
+'''.replace('COUNT',adapt(assigned)).replace('CURRENT',adapt(code)).replace('LEGACY',adapt(legacy)))
|
|
+assert s.index('int totalAssigned = Assigned();')>s.index('Investigate(now, globalLimit);')>s.index('Retask(now);')
|
|
+print('PASS: actual full planner selection/commit body, 360 equivalence scenarios, orders/reservations/cooldowns identical; one128-unit scan vs >10000 repeated unit visits; no FPS claim')
|