Files
EscapistsReintroduced/tests/test_infantry_supply_pool.py
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
8.7 KiB
Python

"""Execute actual finite convoy pool methods; mission/service APIs are controlled doubles."""
import os
import re
from pathlib import Path
from policy_harness import ROOT, body, run
P = Path(os.environ.get('ERR_INFANTRY_SUPPLY_POOL_SOURCE', ROOT / 'addons/core/Scripts/Game/ERR/10_AI/ERR_InfantrySupplyPool.c'))
def cpp(source):
source = source.replace('protected ', '').replace('null', 'nullptr')
source = re.sub(r'foreach \((\w+) (\w+) : ([^)]+)\)', r'for (\1 \2 : \3)', source)
for name in ('ERR_HQ', 'ERR_GameMode', 'ERR_ConvoyService', 'ERR_ConvoyTask', 'ERR_InfantryResupply'):
source = re.sub(r'\b' + name + r' (?=\w)', name + '* ', source)
for name in ('Replication', 'ERR_GameMode', 'ERR_ForceRules', 'Math'):
source = source.replace(name + '.', name + '::')
for name in ('m_HQ', 'm_Mode', 'service', 'retained', 'convoys'):
source = source.replace(name + '.', name + '->')
source = source.replace('int trucks, working, moved, stock, mines;', 'int trucks=0, working=0, moved=0, stock=0, mines=0;')
source = source.replace('string.Format', 'Format')
return source
methods = '\n'.join(cpp(body(P, signature)) for signature in (
'void ERR_InfantrySupplyPool(', 'int Count(', 'ERR_InfantryResupply At(',
'ERR_InfantryResupply First(', 'protected bool Known(', 'bool Unresolved(', 'void Tick(', 'string Status(', 'void Stop('))
methods = methods.replace('void ERR_InfantrySupplyPool(', 'Pool(')
run(r'''
#include <cassert>
#include <string>
#include <vector>
#include <map>
#include <cmath>
#include <limits>
using string=std::string;
template<class T>struct Array:std::vector<T>{int Count(){return this->size();}void Insert(T value){this->push_back(value);}};
template<class V>struct Map{std::map<string,V>values;void Set(string key,V value){values[key]=value;}V Get(string key){return values[key];}};
struct Replication{static inline bool server=true;static bool IsServer(){return server;}};
struct ERR_ForceRules{static bool Finite(float value){return std::isfinite(value);}};
struct Math{static int Min(int a,int b){return a<b?a:b;}};
string Format(string,int trucks,int working,int moved,int stock,int mines){return std::to_string(trucks)+":"+std::to_string(working)+":"+std::to_string(moved)+":"+std::to_string(stock)+":"+std::to_string(mines);}
struct ERR_GameMode{bool active=true;static inline ERR_GameMode* current=nullptr;static ERR_GameMode* Get(){return current;}bool Active(){return active;}};
struct ERR_ConvoyTask{};
struct ERR_ConvoyService{Array<ERR_ConvoyTask*>tasks;int reads=0;int Count(){return tasks.Count();}ERR_ConvoyTask* At(int index){reads++;assert(index>=0&&index<tasks.Count());return tasks[index];}};
struct ERR_HQ{ERR_ConvoyService* convoys=nullptr;ERR_ConvoyService* Convoys(){return convoys;}};
struct ERR_InfantryResupply{
static inline int made=0;static inline bool bindAllowed=true;ERR_ConvoyTask* task=nullptr;Map<int>* consumed=nullptr;Map<float>* next=nullptr;
int polls=0;bool unresolved=false;bool Unresolved(){return unresolved;}int slot=-1,binds=0,ticks=0,stopCalls=0,state=1,moved=0,stock=48;bool stopped=false;
ERR_InfantryResupply(ERR_HQ*,ERR_GameMode*){made++;}
bool Bind(ERR_ConvoyTask* t,int i,Map<int>* c,Map<float>* n){assert(!task);binds++;if(!bindAllowed)return false;task=t;slot=i;consumed=c;next=n;return true;}
ERR_ConvoyTask* Task(){return task;}
void Tick(float){polls++;if(!stopped)ticks++;}
void Stop(){stopCalls++;stopped=true;}
int mines=0;int MinesDelivered(){return mines;}int State(){return state;}int Delivered(){return moved;}int StockCount(){return stock;}
};
struct Pool{
ERR_HQ* m_HQ=nullptr;ERR_GameMode* m_Mode=nullptr;Array<ERR_InfantryResupply*>m_Services;
Map<int> consumed;Map<float> next;Map<int>* m_Consumed=&consumed;Map<float>* m_NextAttempt=&next;bool m_Stopped=false;
'''+methods+r'''
};
struct Fixture{
ERR_GameMode mode;ERR_HQ hq;ERR_ConvoyService convoys;Pool pool;ERR_ConvoyTask tasks[25];
Fixture():pool(&hq,&mode){ERR_GameMode::current=&mode;Replication::server=true;hq.convoys=&convoys;}
void Add(int n){for(int i=0;i<n;i++)convoys.tasks.Insert(&tasks[i]);}
};
int main(){
// No pool activity outside the exact current active server mission or with invalid time.
{Fixture f;f.Add(2);Replication::server=false;f.pool.Tick(0);assert(f.pool.Count()==0);Replication::server=true;
f.mode.active=false;f.pool.Tick(0);assert(f.pool.Count()==0);f.mode.active=true;
ERR_GameMode other;ERR_GameMode::current=&other;f.pool.Tick(0);assert(f.pool.Count()==0);ERR_GameMode::current=&f.mode;
for(float t:{-1.0f,std::numeric_limits<float>::infinity(),std::numeric_limits<float>::quiet_NaN()})f.pool.Tick(t);
assert(f.pool.Count()==0);f.hq.convoys=nullptr;f.pool.Tick(0);assert(f.pool.Count()==0);}
{ERR_GameMode mode;ERR_GameMode::current=&mode;Pool missingHQ(nullptr,&mode);missingHQ.Tick(0);assert(missingHQ.Count()==0);
ERR_HQ hq;Pool missingMode(&hq,nullptr);missingMode.Tick(0);assert(missingMode.Count()==0);}
// Empty discovery stays idle; later existing tasks bind independently exactly once.
{Fixture f;f.pool.Tick(0);assert(f.pool.Count()==0&&!f.pool.First()&&!f.pool.At(-1)&&!f.pool.At(0));
f.Add(2);f.pool.Tick(1000);auto a=f.pool.At(0);auto b=f.pool.At(1);
assert(f.pool.Count()==2&&f.pool.First()==a&&a!=b&&a->task==&f.tasks[0]&&b->task==&f.tasks[1]);
assert(a->slot==0&&b->slot==1&&a->binds==1&&b->binds==1&&a->ticks==1&&b->ticks==1);
f.pool.Tick(2000);assert(f.pool.At(0)==a&&f.pool.At(1)==b&&a->binds==1&&b->binds==1&&a->ticks==2&&b->ticks==2);
// Delivered demand consumption/cooldown state has exact shared identity across trucks.
assert(a->consumed==b->consumed&&a->next==b->next);a->consumed->Set("patrol",17);a->next->Set("patrol",180000);
assert(b->consumed->Get("patrol")==17&&b->next->Get("patrol")==180000);
a->state=3;a->moved=2;b->moved=4;b->stock=8;assert(f.pool.Status()=="2:1:6:56:0");
b->state=7;b->mines=1;assert(f.pool.Status()=="2:2:6:56:1");
f.pool.Stop();assert(a->stopped&&b->stopped&&a->stopCalls==1&&b->stopCalls==1);f.pool.Stop();f.Add(1);f.pool.Tick(3000);
assert(f.pool.Count()==2&&a->ticks==2&&b->ticks==2&&a->stopCalls==1&&b->stopCalls==1);}
// Stopped and detached originals still receive bounded proof polls, never new work.
{Fixture f;f.Add(1);f.pool.Tick(0);auto a=f.pool.At(0);a->unresolved=true;assert(f.pool.Unresolved());
f.convoys.tasks[0]=&f.tasks[1];f.pool.Tick(1000);assert(a->stopped&&a->polls==2&&a->ticks==1&&f.pool.Unresolved());
f.pool.Stop();int made=ERR_InfantryResupply::made;f.pool.Tick(2000);assert(a->polls==3&&a->ticks==1&&ERR_InfantryResupply::made==made);
a->unresolved=false;assert(!f.pool.Unresolved());}
// A stopped empty pool can never create service work later.
{Fixture f;f.pool.Stop();f.Add(2);f.pool.Tick(0);assert(f.pool.Count()==0);}
// A refused binding remains a stopped terminal slot, never ticking or retrying fallback stock.
{Fixture f;f.Add(1);ERR_InfantryResupply::bindAllowed=false;f.pool.Tick(0);auto rejected=f.pool.At(0);
assert(rejected&&rejected->stopped&&rejected->binds==1&&rejected->ticks==0&&!rejected->task);
ERR_InfantryResupply::bindAllowed=true;f.pool.Tick(1000);
assert(f.pool.At(0)==rejected&&rejected->binds==1&&rejected->ticks==0&&!rejected->task);}
// Maximum immutable slot span is twenty, even if malformed source offers more.
{Fixture f;f.Add(25);int made=ERR_InfantryResupply::made;f.pool.Tick(0);
assert(f.pool.Count()==20&&ERR_InfantryResupply::made==made+20&&f.convoys.reads==20&&!f.pool.At(20));
f.pool.Tick(1000);assert(f.pool.Count()==20&&ERR_InfantryResupply::made==made+20&&f.convoys.reads==40);}
// Sparse source identities do not shift services or substitute a later donor for First().
{Fixture f;f.convoys.tasks.Insert(nullptr);f.convoys.tasks.Insert(&f.tasks[1]);f.pool.Tick(0);
auto second=f.pool.At(1);assert(f.pool.Count()==2&&!f.pool.At(0)&&!f.pool.First()&&second&&second->slot==1);
f.convoys.tasks[0]=&f.tasks[0];f.pool.Tick(1000);assert(f.pool.At(0)&&f.pool.First()==f.pool.At(0)&&f.pool.At(1)==second);}
// Repeated source identity never creates duplicate stock ownership.
{Fixture f;f.Add(1);f.convoys.tasks.Insert(&f.tasks[0]);f.pool.Tick(0);assert(f.pool.At(0)&&!f.pool.At(1));
f.pool.Tick(1000);assert(!f.pool.At(1));}
// Replacing/removing a slot stops only its original service; no fresh stock is adopted.
{Fixture f;f.Add(2);f.pool.Tick(0);auto a=f.pool.At(0);auto b=f.pool.At(1);
f.convoys.tasks[0]=&f.tasks[2];f.pool.Tick(1000);assert(a->stopped&&a->task==&f.tasks[0]&&a->ticks==1&&b->ticks==2&&f.pool.At(0)==a);
f.convoys.tasks[0]=&f.tasks[0];f.pool.Tick(2000);assert(a->ticks==1&&a->binds==1&&b->ticks==3);
f.convoys.tasks.resize(1);f.pool.Tick(3000);assert(b->stopped&&f.pool.Count()==2&&f.pool.At(1)==b&&b->task==&f.tasks[1]);}
}
''')
print('PASS: actual finite convoy supply pool; immutable slots, shared demand state, sparse/capped discovery and permanent shutdown')