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

102 lines
8.9 KiB
Python

"""Execute delivered convoy cover rules and the canonical physical executor."""
import os,re
from pathlib import Path
from policy_harness import ROOT,body,run
rules=Path(os.environ.get('ERR_CONVOY_COMBAT',str(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyCombat.c')))
service=Path(os.environ.get('ERR_CONVOY_SERVICE',str(ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_ConvoyService.c')))
accept=body(rules,'static bool Accept(')
for cls in ['ERR_ConvoyCombatState','ERR_ConvoyCombatOrder']:accept=accept.replace(cls+' ',cls+'* ')
accept=accept.replace('state.','state->').replace('order.','order->').replace('ERR_ForceRules.','ERR_ForceRules::')
preamble=r'''
#include <cassert>
#include <vector>
#include <string>
#include <algorithm>
#include <cmath>
using string=std::string;
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();}};
struct Group;
struct ERR_ConvoyCombatState{bool Held=false;int LastCommandId=0,ReportId=0;float ObservedAt=-1;};
struct ERR_ConvoyCombatOrder{int CommandId=0,ReportId=0,VehicleIndex=0;string SourceId;Group* Group=nullptr;bool Hold=false;float ObservedAt=-1,IssuedAt=-1,ExpiresAt=-1;};
namespace ERR_ForceRules{bool Finite(float n){return std::isfinite(n);}}
'''.replace('Group* Group','struct Group* Group')
run(preamble+'struct Rules{'+accept+r'''};
int main(){
ERR_ConvoyCombatState state;ERR_ConvoyCombatOrder hold;hold.CommandId=1;hold.ReportId=10;hold.Hold=true;hold.ObservedAt=100;hold.IssuedAt=1000;hold.ExpiresAt=31000;
assert(Rules::Accept(&state,&hold,1000));assert(!Rules::Accept(&state,&hold,31000));assert(!Rules::Accept(&state,&hold,999));
auto invalid=hold;invalid.ExpiresAt=31001;assert(!Rules::Accept(&state,&invalid,1000));invalid=hold;invalid.ObservedAt=NAN;assert(!Rules::Accept(&state,&invalid,1000));assert(!Rules::Accept(&state,&hold,NAN));
invalid=hold;invalid.IssuedAt=60101;invalid.ExpiresAt=70101;assert(!Rules::Accept(&state,&invalid,60101));
state.Held=true;state.LastCommandId=1;state.ReportId=10;state.ObservedAt=100;assert(!Rules::Accept(&state,&hold,1000));
auto refresh=hold;refresh.CommandId=2;refresh.ReportId=11;refresh.ObservedAt=200;assert(Rules::Accept(&state,&refresh,1000));refresh.ObservedAt=99;assert(!Rules::Accept(&state,&refresh,1000));
refresh=hold;refresh.CommandId=2;refresh.ObservedAt=101;assert(!Rules::Accept(&state,&refresh,1000));refresh=hold;refresh.CommandId=2;refresh.ReportId=9;assert(!Rules::Accept(&state,&refresh,1000));
auto resume=hold;resume.CommandId=3;resume.Hold=false;resume.IssuedAt=60100;resume.ExpiresAt=90100;assert(Rules::Accept(&state,&resume,60100));
resume.IssuedAt=60099;resume.ExpiresAt=90099;assert(!Rules::Accept(&state,&resume,60100));resume.IssuedAt=60100;resume.ExpiresAt=90100;
resume.ReportId=11;assert(!Rules::Accept(&state,&resume,60100));resume.ReportId=10;resume.ObservedAt=101;assert(!Rules::Accept(&state,&resume,60100));resume.ObservedAt=100;state.Held=false;assert(!Rules::Accept(&state,&resume,60100));
assert(!Rules::Accept(nullptr,&hold,1000)&&!Rules::Accept(&state,nullptr,1000));
}
''')
code=body(service,'bool ApplyCombatOrder(')
for cls in ['ERR_ConvoyTask','ERR_ConvoyCombatOrder','ERR_ConvoyVehicle','ERR_ForceRecord','AIAgent']:
code=re.sub(r'\b'+cls+r' ([A-Za-z_]\w*)',cls+r'* \1',code)
code=code.replace('array<AIAgent>','Array<AIAgent*>').replace('foreach (','for (')
for name in ['task','order','unit','record','agent','m_Mode','intercept']:code=code.replace(name+'.',name+'->')
code=code.replace('Receipt.','Receipt->').replace('Combat.','Combat->').replace('Route.','Route->').replace('Group.','Group->').replace('Forces().','Forces()->')
for cls in ['ERR_ConvoyCombatRules','ERR_ConvoyRules','ERR_ForceKind','ERR_HQ']:code=code.replace(cls+'.',cls+'::')
code=code.replace('string.Format','Format')
run(preamble+r'''
struct IEntity{};struct AIAgent{IEntity* entity;IEntity* GetControlledEntity(){return entity;}};
struct Group{Array<AIAgent*>agents;void GetAgents(Array<AIAgent*>&out){out=agents;}};
struct ERR_ForceRoute{int Count(){return 10;}};
enum class ERR_ForceKind{CONVOY,GARRISON};
struct ERR_ForceRecord{bool Physical=true,Eliminated=false;ERR_ForceKind Kind=ERR_ForceKind::CONVOY;int Id=3;};
struct Ledger{ERR_ForceRecord record;ERR_ForceRecord* FindGroup(Group*){return &record;}};
struct Mode{Ledger ledger;Ledger* Forces(){return &ledger;}};
struct ERR_ConvoyReceipt{bool Consumed=true;string SourceId="convoy-1";Array<int>Records{3};};
struct ERR_ConvoyVehicle{IEntity vehicle;IEntity* Entity=&vehicle;bool SupplyHeld=false,Registered=true,Released=false,Arrived=false,valid=true,waypoint=true,failOrder=false;int InterceptToken=0,Index=0,RouteIndex=2,clears=0,orders=0,releases=0;struct Group* Group;Array<IEntity*>Members;ERR_ConvoyCombatState state;ERR_ConvoyCombatState* Combat=&state;bool CrewValid(Mode*){return valid;}void ClearOrder(){clears++;waypoint=false;}bool Order(ERR_ForceRoute*,float){if(waypoint)return true;orders++;if(failOrder)return false;waypoint=true;return true;}void Release(Mode*){releases++;Released=true;Combat->Held=false;}};
struct ERR_ConvoyTask{bool Stopped=false,Ready=true;ERR_ConvoyReceipt* Receipt;ERR_ForceRoute* Route;Array<ERR_ConvoyVehicle*>Vehicles;};
namespace ERR_ConvoyRules{bool captured=false;bool Captured(IEntity*,Group*){return captured;}};
struct ERR_ConvoyCombatRules{'''+accept+r'''};
string Format(string,int,int,int,bool,int){return "event";}
namespace ERR_HQ{int events=0;void Event(string,string){events++;}}
struct Service{Mode* m_Mode;bool current=true;Array<ERR_ConvoyTask*>m_Tasks;bool Current(){return current;}
'''+code+r'''};
struct Case{Mode mode;Service service{&mode};ERR_ConvoyReceipt receipt;ERR_ForceRoute route;ERR_ConvoyTask task;ERR_ConvoyVehicle unit;Group group;IEntity member;AIAgent agent{&member};ERR_ConvoyCombatOrder command;
Case(){unit.Group=&group;unit.Members={&member};group.agents={&agent};task.Receipt=&receipt;task.Route=&route;task.Vehicles={&unit};service.m_Tasks={&task};command.CommandId=1;command.ReportId=10;command.SourceId=receipt.SourceId;command.Group=&group;command.Hold=true;command.ObservedAt=100;command.IssuedAt=1000;command.ExpiresAt=31000;}
};
int main(){
Case supply;assert(supply.service.ApplyCombatOrder(&supply.task,&supply.command,1000));
supply.unit.SupplyHeld=true;supply.command.CommandId=2;supply.command.Hold=false;supply.command.IssuedAt=60100;supply.command.ExpiresAt=90100;
assert(supply.service.ApplyCombatOrder(&supply.task,&supply.command,60100));assert(!supply.unit.Combat->Held&&!supply.unit.waypoint&&supply.unit.orders==0&&supply.unit.releases==0);
Case ok;assert(ok.service.ApplyCombatOrder(&ok.task,&ok.command,1000));assert(ok.unit.Combat->Held&&ok.unit.clears==1&&!ok.unit.waypoint&&ok.unit.RouteIndex==2);
assert(!ok.service.ApplyCombatOrder(&ok.task,&ok.command,1001)&&ok.unit.clears==1);ok.command.CommandId=2;ok.command.ReportId=11;ok.command.ObservedAt=200;
assert(ok.service.ApplyCombatOrder(&ok.task,&ok.command,1002)&&ok.unit.clears==1&&ok.unit.Combat->ReportId==11);
ok.command.Hold=false;ok.command.CommandId=3;ok.command.IssuedAt=60200;ok.command.ExpiresAt=90200;
assert(ok.service.ApplyCombatOrder(&ok.task,&ok.command,60200)&&!ok.unit.Combat->Held&&ok.unit.waypoint&&ok.unit.orders==1&&ok.unit.RouteIndex==2);
assert(!ok.service.ApplyCombatOrder(&ok.task,&ok.command,60201)&&ok.unit.orders==1);
for(int failure=0;failure<15;failure++){
Case c;Group stranger;IEntity foreign;AIAgent foreignAgent{&foreign};
if(failure==0)c.service.current=false;
if(failure==1)c.service.m_Tasks.clear();
if(failure==2)c.task.Stopped=true;
if(failure==3)c.receipt.Consumed=false;
if(failure==4)c.command.SourceId="other";
if(failure==5)c.command.Group=&stranger;
if(failure==6)c.unit.valid=false;
if(failure==7)c.unit.Registered=false;
if(failure==8)c.unit.Released=true;
if(failure==9)c.mode.ledger.record.Physical=false;
if(failure==10)c.mode.ledger.record.Id=4;
if(failure==11)c.group.agents={&foreignAgent};
if(failure==12)c.command.VehicleIndex=1;
if(failure==13)ERR_ConvoyRules::captured=true;
if(failure==14)c.receipt.Records.clear();
assert(!c.service.ApplyCombatOrder(&c.task,&c.command,1000)&&c.unit.clears==0&&c.unit.orders==0&&!c.unit.Combat->Held&&c.unit.Combat->LastCommandId==0);
ERR_ConvoyRules::captured=false;
}
Case failed;assert(failed.service.ApplyCombatOrder(&failed.task,&failed.command,1000));failed.unit.failOrder=true;failed.command.Hold=false;failed.command.CommandId=2;failed.command.IssuedAt=60100;failed.command.ExpiresAt=90100;
assert(!failed.service.ApplyCombatOrder(&failed.task,&failed.command,60100)&&failed.unit.Released&&failed.unit.releases==1&&failed.unit.Combat->LastCommandId==2);
assert(!failed.service.ApplyCombatOrder(&failed.task,&failed.command,60101)&&failed.unit.orders==1&&failed.unit.releases==1);
}
''')
print('PASS: actual delivered cover freshness/lineage/replay and canonical executor; owned waypoint-only hold, one resume at preserved route index, invalid ownership/crew/ledger refusal and fail-closed native recreation')