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

76 lines
7.0 KiB
Python

"""Run actual canonical convoy receipt and shared ledger-cap methods."""
import os,re
from pathlib import Path
from policy_harness import ROOT,run
p=Path(os.environ.get('ERR_CONVOY_BUDGET',str(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyBudget.c')))
s=p.read_text();s=re.sub(r'//[^\n]*','',s).replace('protected ','').replace('ref ','').replace('class ','struct ')
s=s.replace('array<int>','Array<int>').replace('array<ERR_ConvoyReceipt>','Array<ERR_ConvoyReceipt*>')
for typ in ['ERR_GameMode','ERR_ForceLedger','ERR_ForceRecord','ERR_ConvoyReceipt']:
s=re.sub(r'\b'+typ+r' ([A-Za-z_]\w*)',typ+r'* \1',s)
for obj in ['m_Mode','ledger','record','receipt','known']:s=s.replace(obj+'.',obj+'->')
for cls in ['Math','Replication','ERR_GameMode','ERR_ForceKind']:s=s.replace(cls+'.',cls+'::')
s=s.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')
s=re.sub(r'\b(bool|int|[A-Za-z_]\w*\*) ([A-Za-z_]\w*);',r'\1 \2 {};',s)
run(r'''
#include <cassert>
#include <vector>
#include <string>
#include <algorithm>
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>struct Array:std::vector<T>{using std::vector<T>::vector;bool Contains(T v){return std::find(this->begin(),this->end(),v)!=this->end();}void Insert(T v){this->push_back(v);}int Count(){return this->size();}};
namespace Math{int Clamp(int a,int low,int high){return std::clamp(a,low,high);}}
namespace Replication{bool server=true;bool IsServer(){return server;}}
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;}};
''' + s + r'''
int main(){
ERR_GameMode mode;ERR_GameMode::current=&mode;ERR_ForceRecord guard;guard.Id=1;guard.Strength=50;mode.ledger.records.Insert(&guard);
ERR_ConvoyPool pool(&mode,3);assert(pool.BodyLimit()==60);auto receipt=pool.Reserve("road-a");assert(receipt&&pool.CommittedBodies()==60);assert(pool.Reserve("road-a")==receipt);assert(!pool.Reserve("road-b"));
ERR_ConvoyReceipt forged;assert(!pool.Consume(&forged));assert(pool.Consume(receipt)&&!pool.Consume(receipt));
for(int i=0;i<6;i++){
if(i==2)continue;
auto* r=new ERR_ForceRecord();r->Id=i+2;r->InitialStrength=ERR_ConvoyPool::CrewCount(i);r->Strength=r->InitialStrength;r->SourceId="road-a-vehicle-"+std::to_string(i);mode.ledger.records.Insert(r);
assert(pool.BindRecord(receipt,i,r));assert(pool.BindRecord(receipt,i,r));assert(pool.CommittedBodies()==60);
}
ERR_ForceRecord retired;assert(!pool.BindRecord(receipt,2,&retired));
assert(receipt->OutstandingBodies==0&&receipt->OutstandingGroups==0);assert(!pool.Consume(receipt));
// Dead/captured/released forces never refund a consumed receipt's convoy slot.
guard.Strength=0;guard.Eliminated=true;auto second=pool.Reserve("road-b");assert(second);assert(pool.Consume(second));assert(!pool.Consume(receipt));
ERR_GameMode other;ERR_GameMode::current=&other;assert(!pool.Reserve("wrong-world")&&!pool.Consume(second));ERR_GameMode::current=&mode;
ERR_GameMode crowded;ERR_GameMode::current=&crowded;ERR_ForceRecord many;many.Strength=51;crowded.ledger.records.Insert(&many);ERR_ConvoyPool limited(&crowded,8);assert(!limited.Reserve("too-many"));
many.Strength=50;auto reserved=limited.Reserve("exact");assert(reserved);many.Strength=51;assert(!limited.Consume(reserved));many.Strength=50;assert(limited.Consume(reserved));
ERR_ForceRecord invalid;invalid.Id=2;invalid.InitialStrength=2;invalid.Strength=2;invalid.SourceId="exact-vehicle-0";
assert(!limited.BindRecord(reserved,0,&invalid));crowded.ledger.records.Insert(&invalid);invalid.Kind=ERR_ForceKind::GARRISON;assert(!limited.BindRecord(reserved,0,&invalid));invalid.Kind=ERR_ForceKind::CONVOY;
invalid.Strength=1;assert(!limited.BindRecord(reserved,0,&invalid));invalid.Strength=2;invalid.Physical=false;assert(!limited.BindRecord(reserved,0,&invalid));invalid.Physical=true;assert(limited.BindRecord(reserved,0,&invalid));
Replication::server=false;assert(!limited.Reserve("client"));Replication::server=true;
ERR_GameMode formations;ERR_GameMode::current=&formations;for(int i=0;i<124;i++){auto* r=new ERR_ForceRecord();r->Strength=0;r->Physical=false;formations.ledger.records.Insert(r);}ERR_ConvoyPool capped(&formations,8);assert(!capped.Reserve("formation-cap"));
// Five convoy vehicles reserve ten crew; reassigned hunter infantry retains shared headroom.
for(int target:{0,1,3,20}){
ERR_GameMode supply;ERR_GameMode::current=&supply;ERR_ForceRecord baseline;baseline.Strength=60;supply.ledger.records.Insert(&baseline);ERR_ConvoyPool full(&supply,target,true);assert(full.BodyLimit()==60+25*target);
for(int i=0;i<target;i++){
auto ticket=full.Reserve("supply-"+std::to_string(i));assert(ticket&&ticket->OutstandingBodies==10&&ticket->OutstandingGroups==5&&ticket->Records.Count()==6);assert(full.Consume(ticket));
for(int group=0;group<6;group++){
if(group==2)continue;
auto r=new ERR_ForceRecord();r->Id=1+supply.ledger.records.Count();r->SourceId=ticket->SourceId+"-vehicle-"+std::to_string(group);r->InitialStrength=full.PlannedCount(group);r->Strength=r->InitialStrength;supply.ledger.records.Insert(r);assert(full.BindRecord(ticket,group,r));
}
assert(ticket->OutstandingBodies==0&&ticket->OutstandingGroups==0&&full.CommittedBodies()==60+10*(i+1));
}
assert(!full.Reserve("excess"));
for(auto r:supply.ledger.records)r->Strength=0;
assert(!full.Reserve("replacement"));
}
// Reconstructed normal-roster budget boundaries: defaults remain finite, never forced.
for(int baseline:{123,126}){
ERR_GameMode observed;ERR_GameMode::current=&observed;ERR_ForceRecord base;base.Strength=baseline;observed.ledger.records.Insert(&base);
ERR_ConvoyPool normal(&observed,3,true);assert(normal.BodyLimit()==135&&normal.PlannedBodies()==10);
auto first=normal.Reserve("observed-first");assert(bool(first)==(baseline==123));
if(first){assert(normal.Consume(first));assert(normal.CommittedBodies()==133);assert(!normal.Reserve("observed-second"));}
else assert(normal.CommittedBodies()==126);
}
ERR_GameMode finite;ERR_GameMode::current=&finite;ERR_ConvoyPool one(&finite,1);assert(one.Reserve("first"));assert(!one.Reserve("second"));
}
''')
print('PASS: actual convoy receipt identity/one-use, shared60-body and128-formation caps, outstanding/native-ledger accounting, exact10-body composition, source reuse, mode/client guards and no consumed receipt refunds')