Files
EscapistsReintroduced/tests/test_supply_budget_admission.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

116 lines
10 KiB
Python

"""Actual startup and pool accounting; terrain and native dispatch boundaries doubled."""
from policy_harness import ROOT, body, run
from pathlib import Path
import os,re
P=Path(os.environ.get('ERR_SUPPLY_SYSTEM_SOURCE',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_SupplyConvoySystem.c'))
s='\n'.join(body(P,sig) for sig in ('bool StartOne(', 'protected string RefusalReason(',
'protected string BudgetStatus(', 'protected void NoteFailure(', 'void Tick(', 'string Status('))
s=re.sub(r'//[^\n]*','',s).replace('protected ','').replace('null','nullptr')
for name in ('Replication','ERR_GameMode','Math','ERR_Actors','ERR_ConvoyRoadPlan','ERR_SupplyConvoyRules','ERR_HQ','ERR_ForceRules','string'):
s=s.replace(name+'.',name+'::')
s=s.replace('string::Empty','string{}').replace('array<vector>','Array<vector>')
for typ in ('ERR_ForceRoute','ERR_ConvoyReceipt','ERR_ConvoyTask'):
s=re.sub(r'\b'+typ+r' (?=\w)',typ+'* ',s)
for name in ('m_Mode','m_Pool','m_Service','receipt','task'):
s=s.replace(name+'.',name+'->')
s=s.replace('GetGame().','GetGame()->').replace('GetWorldEntity().','GetWorldEntity()->').replace('GetWorld().','GetWorld()->')
s=s.replace('(m_Service->Count() + 1).ToString()', 'std::to_string(m_Service->Count() + 1)')
B=Path(os.environ.get('ERR_CONVOY_BUDGET',ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyBudget.c'))
# Execute the real reservation/replay/accounting/consume methods alongside StartOne.
pool=B.read_text();pool=re.sub(r'//[^\n]*','',pool).replace('protected ','').replace('ref ','').replace('class ','struct ')
pool=pool.replace('array<int>','Array<int>').replace('array<ERR_ConvoyReceipt>','Array<ERR_ConvoyReceipt*>')
for typ in ('ERR_GameMode','ERR_ForceLedger','ERR_ForceRecord','ERR_ConvoyReceipt'):
pool=re.sub(r'\b'+typ+r' ([A-Za-z_]\w*)',typ+r'* \1',pool)
for obj in ('m_Mode','ledger','record','receipt','known'):pool=pool.replace(obj+'.',obj+'->')
for cls in ('Math','Replication','ERR_GameMode','ERR_ForceKind'):pool=pool.replace(cls+'.',cls+'::')
pool=pool.replace('Forces().','Forces()->').replace('foreach (','for (').replace('null','nullptr').replace('void ERR_ConvoyPool(','ERR_ConvoyPool(').replace('vehicle.ToString()','std::to_string(vehicle)').replace('\n}\n','\n};\n')
pool=re.sub(r'\b(bool|int|[A-Za-z_]\w*\*) ([A-Za-z_]\w*);',r'\1 \2 {};',pool)
pool=pool.replace('ERR_ConvoyReceipt* Reserve(string sourceId)\n {','ERR_ConvoyReceipt* Reserve(string sourceId)\n {\n calls.reserve++;')
run(r'''
#include <cassert>
#include <cmath>
#include <algorithm>
#include <string>
#include <sstream>
#include <vector>
struct string:std::string{using std::string::string;string(std::string s):std::string(s){}bool IsEmpty(){return empty();}int Length(){return size();}
template<class T>static std::string Text(T v){std::ostringstream o;o<<v;return o.str();}
template<class...T>static string Format(string format,T... values){int i=0;auto replace=[&](auto v){auto tag="%"+std::to_string(++i);auto at=format.find(tag);if(at!=std::string::npos)format.replace(at,tag.size(),Text(v));};(replace(values),...);return format;}};
struct vector{float x=0,y=0,z=0;float operator[](int i)const{return i==0?x:i==1?y:z;}};
std::ostream& operator<<(std::ostream& out,vector p){return out<<p.x<<","<<p.y<<","<<p.z;}
vector Vector(float x,float y,float z){return {x,y,z};}
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;int Count(){return this->size();}bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}void Insert(T x){this->push_back(x);}};
struct Counts{int world=0,bounds=0,bases=0,ground=0,random=0,road=0,reserve=0,dispatch=0;} calls;
struct World{void GetWorldBounds(vector& low,vector& high){calls.bounds++;low={0,0,0};high={10000,100,10000};}};
struct Game{World world;bool available=true;float now=0;World* GetWorldEntity(){calls.world++;return available?&world:nullptr;}Game* GetWorld(){return this;}float GetWorldTime(){return now;}} game;
Game* GetGame(){return &game;}
enum class ERR_ForceKind{GARRISON,CONVOY};
struct ERR_ForceRecord{int Id=0,InitialStrength=0,Strength=0;bool Physical=true,Eliminated=false;void* Group=this;ERR_ForceKind Kind=ERR_ForceKind::CONVOY;string SourceId;};
struct ERR_ForceLedger{Array<ERR_ForceRecord*>records;int Count(){return records.Count();}ERR_ForceRecord* At(int i){return records[i];}ERR_ForceRecord* Find(int id){for(auto* r:records){if(r->Id==id)return r;}return nullptr;}};
struct ERR_GameMode{static inline ERR_GameMode* current=nullptr;ERR_ForceLedger ledger;bool active=true;static ERR_GameMode* Get(){return current;}bool Active(){return active;}ERR_ForceLedger* Forces(){return &ledger;}};
using Mode=ERR_GameMode;
namespace Replication{bool server=true;bool IsServer(){return server;}}
namespace Math{int Clamp(int v,int low,int high){return std::clamp(v,low,high);}float Min(float a,float b){return std::min(a,b);}float Max(float a,float b){return std::max(a,b);}float RandomFloat(float low,float){calls.random++;return low;}}
namespace ERR_Actors{vector Ground(vector p){calls.ground++;return p;}}
namespace ERR_ForceRules{bool Finite(float v){return std::isfinite(v);}}
namespace ERR_HQ{std::vector<string> events;void Event(string,string detail){events.push_back(detail);}}
struct ERR_ForceRoute{};
'''+pool+r'''
using Pool=ERR_ConvoyPool;
struct ERR_ConvoyTask{bool Stopped=false;};
struct Service{Pool* pool=nullptr;int count=0;bool fail=false;ERR_ConvoyTask task;int Count(){return count;}
ERR_ConvoyTask* Dispatch(ERR_ConvoyReceipt* r,Array<vector>,Array<vector>,ERR_ForceRoute*,float){calls.dispatch++;if(fail||!pool->Consume(r))return nullptr;count++;return &task;}};
namespace ERR_ConvoyRoadPlan{bool clear=true;ERR_ForceRoute found;
bool Build(vector,Array<vector>& positions,Array<vector>& angles,ERR_ForceRoute*& route,Array<vector>,float){calls.road++;if(!clear)return false;positions={{100,0,100}};angles={{0,0,0}};route=&found;return true;}}
namespace ERR_SupplyConvoyRules{bool Separated(vector,Array<vector>,float){return true;}}
struct Startup{Mode* m_Mode;Pool* m_Pool;Service* m_Service;Array<vector> m_Bases,m_Starts;int m_Target=3,m_Attempts=0;bool m_Stopped=false,m_Automatic=true;
float m_Began=-1,m_Next=0;string m_Reason,m_LastFailure;void FindBases(){calls.bases++;m_Bases={{0,0,0},{1000,0,0}};}
'''+s+r'''
};
void Reset(){calls={};ERR_HQ::events.clear();ERR_ConvoyRoadPlan::clear=true;game.available=true;}
void NoSurvey(){assert(calls.bases==0&&calls.world==0&&calls.bounds==0&&calls.ground==0&&calls.random==0&&calls.road==0&&calls.reserve==0&&calls.dispatch==0);}
struct Fixture{Mode mode;ERR_ForceRecord baseline;Pool pool;Service service;Startup startup;
Fixture(int bodies):pool(&mode,3,true),startup{&mode,&pool,&service}{baseline.Strength=bodies;mode.ledger.records.Insert(&baseline);ERR_GameMode::current=&mode;service.pool=&pool;Reset();}};
int main(){
{Fixture f(126);auto& blocked=f.startup;assert(!blocked.StartOne());NoSurvey();
assert(blocked.m_Reason=="insufficient shared body budget for another original convoy");
assert(blocked.m_Starts.empty()&&blocked.m_Attempts==0&&f.service.count==0&&f.pool.m_Receipts.empty());
blocked.Tick(0,true);blocked.Tick(30000,true);blocked.Tick(34999,true);blocked.Tick(35000,true);
NoSurvey();assert(blocked.m_Attempts==2&&ERR_HQ::events.size()==1);
assert(ERR_HQ::events[0].find("126/135")!=string::npos&&ERR_HQ::events[0].find("requires 10")!=string::npos);
for(int i=2;i<150;i++)blocked.Tick(30000+5000*i,true);
NoSurvey();assert(blocked.m_Attempts==120&&ERR_HQ::events.size()==2);
assert(ERR_HQ::events.back().find("attempts exhausted")!=string::npos);}
{Fixture f(125);assert(f.startup.StartOne());assert(calls.bases==1&&calls.bounds==1&&calls.road==1&&calls.reserve==1&&calls.dispatch==1);
assert(f.service.count==1&&f.startup.m_Starts.Count()==1&&f.pool.m_Receipts[0]->Consumed&&f.pool.CommittedBodies()==135);
// A lost task counter must never turn the same consumed source into a replacement.
f.service.count=0;assert(!f.startup.StartOne());assert(calls.dispatch==1&&f.startup.m_Reason.find("already consumed")!=string::npos);}
{Fixture f(100);ERR_ConvoyRoadPlan::clear=false;assert(!f.startup.StartOne());assert(calls.road==8&&calls.reserve==0&&calls.dispatch==0);
assert(f.startup.m_Reason=="no clear separated road formation in this bounded survey");}
{Fixture f(126);f.startup.Tick(0,true);f.startup.Tick(30000,true);NoSurvey();f.baseline.Strength=125;
f.startup.Tick(35000,true);assert(f.service.count==1&&calls.dispatch==1&&f.startup.m_Attempts==2&&f.startup.m_LastFailure.empty());}
// Critical real-pool replay: failed road preflight retains the unconsumed receipt.
{Fixture f(123);f.service.fail=true;assert(!f.startup.StartOne());
assert(f.service.count==0&&f.pool.m_Receipts.Count()==1&&f.pool.CommittedBodies()==133);
auto original=f.pool.m_Receipts[0];assert(!original->Consumed&&original->OutstandingBodies==10);
assert(f.pool.BodyBudgetAvailable("supply-convoy-1")&&!f.pool.BodyBudgetAvailable("supply-convoy-2"));
assert(f.pool.Reserve("supply-convoy-1")==original&&!f.pool.Reserve("supply-convoy-2"));
f.service.fail=false;Reset();assert(f.startup.StartOne());
assert(calls.road==1&&calls.reserve==1&&calls.dispatch==1&&f.pool.m_Receipts.Count()==1);
assert(f.pool.m_Receipts[0]==original&&original->Consumed&&f.service.count==1&&f.pool.CommittedBodies()==133);}
{Fixture f(123);f.service.fail=true;assert(!f.startup.StartOne());auto original=f.pool.m_Receipts[0];
// Precheck intentionally cannot grant admission: actual Consume rechecks budget growth.
f.baseline.Strength=126;f.service.fail=false;assert(!f.startup.StartOne()&&!original->Consumed&&f.service.count==0);
f.baseline.Strength=125;assert(f.startup.StartOne()&&original->Consumed&&f.pool.CommittedBodies()==135);}
{Fixture f(0);assert(!f.pool.BodyBudgetAvailable("")&&!f.pool.BodyBudgetAvailable(string(65,'x')));
Mode other;ERR_GameMode::current=&other;assert(!f.pool.BodyBudgetAvailable("supply-convoy-1"));
ERR_GameMode::current=&f.mode;Replication::server=false;assert(!f.pool.BodyBudgetAvailable("supply-convoy-1"));Replication::server=true;
f.mode.active=false;assert(!f.pool.BodyBudgetAvailable("supply-convoy-1"));}
{Fixture f(0);f.startup.m_Pool=nullptr;assert(!f.startup.StartOne());NoSurvey();assert(f.startup.m_Reason=="convoy allocator unavailable");}
{Fixture f(0);f.startup.m_Target=0;assert(!f.startup.StartOne());NoSurvey();f.startup.m_Target=3;
f.startup.m_Stopped=true;assert(!f.startup.StartOne());NoSurvey();}
}
''')
print('PASS: actual StartOne plus actual pool preserve unconsumed receipt retry, reject consumed replay, skip impossible new-budget surveys, retain equality, retry caps and journal dedup; no FPS claim')