167 lines
14 KiB
Python
167 lines
14 KiB
Python
"""Run the actual bounded placement scheduler with terrain and clock adapters."""
|
|
import re
|
|
from policy_harness import ROOT,body,run
|
|
p=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_GeneratedSites.c'
|
|
s='\n'.join(body(p, signature) for signature in ('static bool StartupSite(', 'void StartupOnly(', 'bool StartupReady(', 'protected bool SkipOptional(', 'protected bool RescueFits(', 'ERR_PrepareResult Tick('))
|
|
s=s.replace('ERR_SiteDefinition site','Site* site').replace('site.','site->').replace('m_Terrain.Site','m_Terrain.SiteAt')
|
|
s=s.replace('System.GetTickCount','Clock::Now').replace('Replication.IsServer','Server').replace('Math.RandomFloat','RandomFloat').replace('ERR_PrepareResult.','Result::')
|
|
s=s.replace('float floor;','float floor=0;').replace('protected ', '').replace('ref ERR_SiteDefinition retained', 'Site* retained').replace('retained.', 'retained->').replace('m_Definition.', 'm_Definition->').replace('ERR_LocationKind.', 'Kind::')
|
|
s=s.replace('foreach (Site* site : m_Definition->Sites)', 'for (Site* site : m_Definition->Sites)')
|
|
s=s.replace('bool fits;', 'bool fits=false;')
|
|
s=s.replace('ERR_Actors.', 'ERR_Actors::').replace('ERR_CampTerrain.', 'ERR_CampTerrain::').replace('ERR_RescueLanding.', 'ERR_RescueLanding::').replace('ERR_AirfieldPlacement.', 'ERR_AirfieldPlacement::')
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <string>
|
|
#include <vector>
|
|
#include <algorithm>
|
|
using string=std::string;
|
|
struct vector{float a[3];float& operator[](int i){return a[i];}};vector Vector(float x,float y,float z){return {{x,y,z}};}
|
|
vector operator+(vector a,vector b){return Vector(a[0]+b[0],a[1]+b[1],a[2]+b[2]);}
|
|
namespace ERR_Actors{vector Ground(vector p){return p;}}
|
|
namespace ERR_CampTerrain{vector Rotate(vector p,float){return p;}}
|
|
namespace ERR_AirfieldPlacement{bool clear=true;int groundChecks=0,airportChecks=0;bool Clear(vector){airportChecks++;return clear;}bool ClearGround(vector){groundChecks++;return clear;}}
|
|
namespace ERR_RescueLanding{bool fits=true;bool Fits(vector){return fits;}}
|
|
namespace Result{enum Value{FAILED=-1,PENDING,READY};}using ERR_PrepareResult=Result::Value;
|
|
namespace Clock {int now=0,step=1;int Now(){int t=now;now+=step;return t;}}
|
|
bool Server(){return true;}float RandomFloat(float a,float b){return (a+b)/2;}template<class...T>void PrintFormat(T...){}
|
|
namespace Kind{enum{PRISON,RADIO,EXTRACTION,OUTPOST,DEPOT,MORTAR,HOSPITAL};}
|
|
struct Site{int Kind=Kind::PRISON;bool required=false;string Label="Test site";vector Position=Vector(10,0,20),AircraftOffset=Vector(12,0,0);float Heading=0;bool PlacementResolved=false,RandomClear=true,RequireAirfield=false;string Id="site";};
|
|
template<class T>struct Array:std::vector<T>{using std::vector<T>::operator=;int Count(){return this->size();}int Find(T v){auto i=std::find(this->begin(),this->end(),v);return i==this->end()?-1:i-this->begin();}void Remove(int i){this->erase(this->begin()+i);}void InsertAt(T v,int i){this->insert(this->begin()+i,v);}};
|
|
struct Definition{bool TerrainGenerated=false;Array<Site*> Sites;Site* required=nullptr;bool Valid(){return !required||Sites.Find(required)>=0;}};
|
|
struct Terrain{string Rejection(){return "terrain";}bool clear=true;Site* refused=nullptr;int queries=0;bool SiteAt(Site* site,vector,float,float& height){queries++;height=42;return clear&&site!=refused;}};
|
|
struct Scheduler{
|
|
void RecordRejection(string){}void ReportSearch(Site*,string){}void ReportProgress(Site*){}void ClearDiagnostics(){}string RejectionSummary(){return "terrain";}
|
|
Definition definition;Definition* m_Definition=&definition;
|
|
bool m_StartupOnly=false;
|
|
bool m_Started=true;ERR_PrepareResult m_Result=Result::PENDING;int m_Index=0,m_Attempts=0,m_Began=0;
|
|
vector m_Low=Vector(0,0,0),m_High=Vector(10000,0,10000);Array<Site*>m_Pending;Terrain m_Terrain;string m_Error;
|
|
bool Separated(Site*,vector,bool){return true;}
|
|
'''+s+r'''
|
|
};
|
|
int main(){
|
|
// Startup pauses before radio/camps, and resuming resets the next-site deadline.
|
|
{Clock::now=0;Scheduler s;Site prison,radio,extraction;radio.Kind=Kind::RADIO;extraction.Kind=Kind::EXTRACTION;extraction.RandomClear=false;
|
|
s.definition.Sites={&prison,&radio,&extraction};s.m_Pending={&prison,&radio};s.StartupOnly(true);
|
|
while(!s.StartupReady()) assert(s.Tick()==Result::PENDING);
|
|
assert(prison.PlacementResolved&&!radio.PlacementResolved);int calls=s.m_Terrain.queries;
|
|
Clock::now=60000;assert(s.Tick()==Result::PENDING&&s.m_Terrain.queries==calls);
|
|
s.StartupOnly(false);assert(s.m_Began>=60000);while(s.Tick()==Result::PENDING){}assert(radio.PlacementResolved);}
|
|
|
|
{Scheduler s;s.m_Started=false;assert(s.Tick()==Result::FAILED);}
|
|
{Scheduler s;assert(s.Tick()==Result::READY);assert(s.m_Terrain.queries==0);}
|
|
{Clock::now=0;Scheduler s;Site a,b;s.m_Pending={&a,&b};while(s.Tick()==Result::PENDING){}assert(s.m_Result==Result::READY&&a.PlacementResolved&&b.PlacementResolved);assert(a.Position[1]>42&&a.Position[1]<42.1);int calls=s.m_Terrain.queries;assert(s.Tick()==Result::READY&&s.m_Terrain.queries==calls);}
|
|
{Clock::now=0;Scheduler s;Site a;s.m_Pending={&a};s.m_Terrain.clear=false;assert(s.Tick()==Result::PENDING);assert(s.m_Terrain.queries<10&&!a.PlacementResolved);Clock::now=30001;assert(s.Tick()==Result::FAILED&&!s.m_Error.empty()&&!a.PlacementResolved);int calls=s.m_Terrain.queries;assert(s.Tick()==Result::FAILED&&s.m_Terrain.queries==calls);}
|
|
{Clock::now=0;Scheduler s;Site a;s.m_Pending={&a};s.m_Attempts=12000;assert(s.Tick()==Result::READY&&a.Position[0]==10&&a.Position[2]==20&&a.PlacementResolved);}
|
|
{Clock::now=30001;Scheduler s;Site a;s.m_Pending={&a};assert(s.Tick()==Result::READY&&a.Position[0]==10&&a.Position[2]==20);}
|
|
// Each optional type can fail after exhausting candidates without blocking the next site.
|
|
for(int kind : {Kind::OUTPOST,Kind::HOSPITAL,Kind::DEPOT}){
|
|
Clock::now=0;Scheduler s;Site a,b;a.Kind=kind;a.Id="unplaceable";b.Id="next";
|
|
s.m_Pending={&a,&b};s.definition.Sites={&a,&b};s.m_Terrain.refused=&a;s.m_Attempts=12000;
|
|
while(s.Tick()==Result::PENDING){}
|
|
assert(s.m_Result==Result::READY&&s.m_Error.empty()&&!a.PlacementResolved&&b.PlacementResolved);
|
|
assert(s.definition.Sites.size()==1&&s.definition.Sites[0]==&b);
|
|
int calls=s.m_Terrain.queries;assert(s.Tick()==Result::READY&&s.m_Terrain.queries==calls);
|
|
}
|
|
// A deadline skip gets a fresh candidate/time budget for the following optional site.
|
|
{Clock::now=30001;Scheduler s;Site a,b;a.Kind=Kind::OUTPOST;b.Kind=Kind::HOSPITAL;
|
|
s.m_Pending={&a,&b};s.definition.Sites={&a,&b};s.m_Terrain.refused=&a;
|
|
while(s.Tick()==Result::PENDING){}assert(s.m_Result==Result::READY&&b.Position[0]!=10&&b.PlacementResolved);}
|
|
// Required objectives (including mortars) still fail; no unsafe terrain is accepted.
|
|
for(int kind : {Kind::PRISON,Kind::RADIO,Kind::EXTRACTION,Kind::MORTAR}){
|
|
Clock::now=0;Scheduler s;Site a;a.Kind=kind;s.m_Pending={&a};s.definition.Sites={&a};s.m_Attempts=12000;s.m_Terrain.clear=false;
|
|
assert(s.Tick()==Result::FAILED&&s.definition.Sites[0]==&a&&!a.PlacementResolved);
|
|
}
|
|
// Optional by type can still be indispensable for minimum accessible radio intel.
|
|
{Clock::now=0;Scheduler s;Site before,a,after;a.Kind=Kind::HOSPITAL;
|
|
s.definition.Sites={&before,&a,&after};s.definition.required=&a;s.m_Pending={&a};s.m_Attempts=12000;s.m_Terrain.clear=false;
|
|
assert(s.Tick()==Result::FAILED&&s.definition.Sites.size()==3&&s.definition.Sites[1]==&a&&!a.PlacementResolved);}
|
|
// Primary pad failures retry before commitment; authored fallback cannot bypass them.
|
|
{Clock::now=0;Scheduler s;Site a;a.Kind=Kind::EXTRACTION;s.m_Pending={&a};ERR_AirfieldPlacement::clear=false;
|
|
assert(s.Tick()==Result::PENDING&&!a.PlacementResolved);s.m_Attempts=12000;
|
|
assert(s.Tick()==Result::FAILED&&!a.PlacementResolved);ERR_AirfieldPlacement::clear=true;}
|
|
{Clock::now=0;Scheduler s;Site a;a.Kind=Kind::EXTRACTION;s.m_Pending={&a};int checks=ERR_AirfieldPlacement::groundChecks;
|
|
while(s.Tick()==Result::PENDING){}assert(a.PlacementResolved&&ERR_AirfieldPlacement::groundChecks>checks&&ERR_AirfieldPlacement::airportChecks==0);}
|
|
// Extraction candidates without secondary fleet space remain uncommitted and are retried.
|
|
{Clock::now=0;Scheduler s;Site a;a.Kind=Kind::EXTRACTION;s.m_Pending={&a};s.definition.Sites={&a};ERR_RescueLanding::fits=false;
|
|
assert(s.Tick()==Result::PENDING&&!a.PlacementResolved);ERR_RescueLanding::fits=true;
|
|
while(s.Tick()==Result::PENDING){}assert(s.m_Result==Result::READY&&a.PlacementResolved);}
|
|
// An authored fallback cannot bypass the new fleet-space qualification.
|
|
{Clock::now=0;Scheduler s;Site a;a.Kind=Kind::EXTRACTION;s.m_Pending={&a};s.definition.Sites={&a};s.m_Attempts=12000;ERR_RescueLanding::fits=false;
|
|
assert(s.Tick()==Result::FAILED&&!a.PlacementResolved);ERR_RescueLanding::fits=true;}
|
|
}
|
|
''')
|
|
print('PASS: actual generation scheduler bounds each tick, enforces deadline/candidate ceiling, validates fallback, commits only accepted positions, skips only dispensable sites, preserves required objectives/intel, and resets the next search budget')
|
|
|
|
# Exercise the production background activation path: waiting for the first
|
|
# player must not consume the next mandatory site's placement budget.
|
|
scenario=ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_ScenarioComponent.c'
|
|
activation=body(scenario, 'protected void BackgroundStep()').split(' if (m_PendingSite)')[0]+'}\n'
|
|
activation=activation.replace('protected ', '').replace('ERR_GameMode mode = ERR_GameMode.Cast(GetOwner());', 'Mode* mode = GetOwner();')
|
|
for a,b in [('Replication.IsServer()', 'true'),('ERR_GameMode.Get()', 'GetOwner()'),('mode.Active()', 'mode->Active()'),('GetGame().GetPlayerManager().GetPlayerCount()', 'players'),('ERR_PrepareResult.', 'Result::')]: activation=activation.replace(a,b)
|
|
assert 'm_Generated.StartupOnly(false)' not in body(scenario,'ERR_PrepareResult Prepare(')
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <string>
|
|
using string=std::string;
|
|
namespace Result{enum Value{FAILED=-1,PENDING,READY};}using ERR_PrepareResult=Result::Value;
|
|
int now=0,players=0;
|
|
struct Mode{bool active=true;bool Active(){return active;}};
|
|
struct Generated{int began=0,resumes=0,ticks=0;bool startup=true;void StartupOnly(bool v){assert(!v);startup=v;began=now;resumes++;}ERR_PrepareResult Tick(){assert(!startup);ticks++;return now-began>=30000?Result::FAILED:Result::PENDING;}string Error(){return "expired";}};
|
|
struct Scenario{Mode mode;Generated m_Generated;bool m_BackgroundActivated=false,m_PlacementDone=false,stopped=false,failed=false;Mode* GetOwner(){return &mode;}bool BackgroundPending(){return true;}void StopBackground(){stopped=true;}void FailBackground(string){failed=true;}
|
|
'''+activation+r'''
|
|
};
|
|
int main(){Scenario s;
|
|
for(now=0;now<3600000;now+=250)s.BackgroundStep();
|
|
assert(s.m_Generated.resumes==0&&s.m_Generated.ticks==0&&!s.failed);
|
|
players=1;s.BackgroundStep();assert(s.m_Generated.resumes==1&&s.m_Generated.ticks==1&&!s.failed&&s.m_Generated.began==now);
|
|
players=0;now+=250;s.BackgroundStep();assert(s.m_Generated.resumes==1&&s.m_Generated.ticks==2&&!s.failed);
|
|
now+=30000;s.BackgroundStep();assert(s.failed); // the active search is still bounded
|
|
Scenario ended;ended.mode.active=false;ended.BackgroundStep();assert(ended.stopped&&ended.m_Generated.resumes==0);
|
|
}
|
|
''')
|
|
print('PASS: actual background activation waits an hour empty without consuming placement time; disconnect does not reset active budget')
|
|
|
|
# Execute actual spacing policy in both placement orders: randomized extraction
|
|
# must not be committed outside the prison's escape boundary.
|
|
spacing=body(p,'protected bool Separated(')
|
|
for before,after in [('protected ',''),('ERR_SiteDefinition candidate','Site* candidate'),('foreach (ERR_SiteDefinition other :','for (Site* other :'),('candidate.','candidate->'),('other.','other->'),('m_Definition.','m_Definition->'),('ERR_LocationKind.','Kind::'),('vector.DistanceXZ','DistanceXZ'),('ERR_GameMode.Get().EscapeRadius()','escapeRadius')]:
|
|
spacing=spacing.replace(before,after)
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <cmath>
|
|
#include <vector>
|
|
struct vector{float x,z;};float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}
|
|
namespace Kind{enum{PRISON,RADIO,EXTRACTION,OUTPOST,DEPOT,MORTAR,HOSPITAL};}
|
|
struct Site{int Kind=Kind::PRISON;bool RandomClear=true,PlacementResolved=true;vector Position{0,0};};
|
|
struct Definition{std::vector<Site*> Sites;};
|
|
struct Placement{Definition definition;Definition* m_Definition=&definition;float escapeRadius=5000;
|
|
'''+spacing+r'''
|
|
};
|
|
int main(){
|
|
for(bool reverse:{false,true}){
|
|
Placement s;Site a,b;a.Kind=reverse?Kind::PRISON:Kind::EXTRACTION;b.Kind=reverse?Kind::EXTRACTION:Kind::PRISON;
|
|
s.definition.Sites={&a,&b};
|
|
assert(s.Separated(&a,{1000,0},false));
|
|
assert(!s.Separated(&a,{4900,0},false));
|
|
assert(!s.Separated(&a,{6000,0},true)); // authored fallback has identical safety
|
|
assert(!s.Separated(&a,{100,0},false));
|
|
b.PlacementResolved=false;assert(s.Separated(&a,{6000,0},false)); // unused authored seed
|
|
}
|
|
}
|
|
''')
|
|
print('PASS: actual separation keeps randomized extraction within the escape boundary in both placement orders, including fallback')
|
|
|
|
# Exercise the production randomized-kind rejection expression, including extraction.
|
|
validation=body(ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_SiteDefinition.c','bool Valid(')
|
|
random_rule=next(line.strip() for line in validation.splitlines() if 'if (site.RandomClear' in line)
|
|
rule=random_rule.replace('ERR_LocationKind.','Kind::')
|
|
run(r'''
|
|
#include <cassert>
|
|
#include <initializer_list>
|
|
namespace Kind{enum{PRISON,RADIO,EXTRACTION,OUTPOST,DEPOT,MORTAR,HOSPITAL};}
|
|
struct Site{bool RandomClear=true;int Kind=Kind::EXTRACTION;};
|
|
bool Accept(Site site){''' + rule + r''' return true;}
|
|
int main(){for(int kind:{Kind::PRISON,Kind::RADIO,Kind::EXTRACTION,Kind::OUTPOST,Kind::DEPOT,Kind::MORTAR,Kind::HOSPITAL}){Site site;site.Kind=kind;assert(Accept(site));}Site invalid;invalid.Kind=99;assert(!Accept(invalid));}
|
|
''')
|
|
print('PASS: scenario validation admits randomized extraction definitions and rejects unsupported randomized site kinds')
|