Files
EscapistsReintroduced/tests/test_radio_intel.py
T
2026-10-21 16:36:22 +02:00

120 lines
12 KiB
Python

"""Run the production radio gate, folder recovery and evidence targeting bodies.
Native inventory/discovery calls are explicit doubles; the retained Workbench
fixture separately exercises real inventory, world observation and tuning.
"""
import re
from policy_harness import ROOT, body, run
core = ROOT/'addons/core/Scripts/Game/ERR/00_Core/ERR_DiscoveryService.c'
evidence = ROOT/'addons/core/Scripts/Game/ERR/20_Intel/ERR_IntelEvidence.c'
def cpp(s):
s=s.replace('protected ', '').replace('foreach (', 'for (').replace('null', 'nullptr')
s=s.replace('ERR_DocumentComponent.Cast(folder.FindComponent(ERR_DocumentComponent))','folder->document')
s=s.replace('InventoryStorageManagerComponent.Cast(player.FindComponent(InventoryStorageManagerComponent))','player->inventory')
s=s.replace('set<string> available = new set<string>();','Set<string> available;').replace('set<string> identified = new set<string>();','Set<string> identified;')
s=s.replace('array<ERR_LocationComponent>', 'Array<ERR_LocationComponent*>').replace('array<IEntity>','Array<IEntity>')
for t in ('ERR_GameMode','ERR_LocationComponent','ERR_SiteService','ERR_SiteRuntime','ERR_DocumentComponent','InventoryStorageManagerComponent','ERR_IssuedIntel','ERR_BodyEvidence'):
s=re.sub(r'\b'+t+r' (\w+)',t+r'* \1',s)
s=s.replace('ERR_SiteRuntime* site;', 'ERR_SiteRuntime* site=nullptr;').replace('bool redirectRadio;', 'bool redirectRadio=false;').replace('bool changed;', 'bool changed=false;').replace('int assigned;', 'int assigned=0;')
for n in ('Replication','ERR_LocationKind','ERR_Actors','SCR_FactionManager','ERR_GameMode','Math'):
s=s.replace(n+'.', n+'::')
for n in ('m_Mode','mode','location','target','chosen','sites','site','document','player','record','issued'):
s=re.sub(r'\b'+n+r'\.', n+'->',s)
for n in ('Settings','Sites','Players','MineIntel','Evidence','Discovery','Get','GetPlayerManager'):
s=s.replace(n+'().',n+'()->')
s=s.replace('site->Definition.', 'site->Definition->').replace('GetGame().','GetGame()->').replace('GetRandomElement().','GetRandomElement()->')
s=s.replace('void ERR_DiscoveryService(', 'Service(')
return s.replace('target->IsEmpty()', 'target.IsEmpty()')
gate='\n'.join(body(core,sig) for sig in ('void ERR_DiscoveryService(', 'protected bool CountsForRadioIntel(', 'protected void RefreshRadioIntel(', 'bool RadioIntelAllowed()', 'bool MarkSpent(', 'protected void Recover('))
ev='\n'.join(body(evidence,sig) for sig in ('protected string NextTarget(', 'string Target(', 'void Retarget('))
run(r'''
#include <cassert>
#include <string>
#include <vector>
#include <map>
#include <set>
#include <algorithm>
struct string:std::string{using std::string::string;using std::string::operator=;string(std::string s):std::string(s){}bool IsEmpty(){return empty();}};
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;void Insert(T x){this->push_back(x);}void Clear(){this->clear();}int Count(){return this->size();}bool IsEmpty(){return this->empty();}bool Contains(T x){return std::find(this->begin(),this->end(),x)!=this->end();}T GetRandomElement(){return this->front();}};
template<class T>struct Set:std::set<T>{void Insert(T x){this->insert(x);}int Count(){return this->size();}bool Contains(T x){return this->count(x);}};
template<class K,class V>struct Map:std::map<K,V>{bool Find(K k,V& v){auto i=this->find(k);if(i==this->end())return false;v=i->second;return true;}void Set(K k,V v){(*this)[k]=v;}};
namespace Replication{bool server=true;bool IsServer(){return server;}}
namespace Math{int Min(int a,int b){return std::min(a,b);}int RandomInt(int,int){return 99;}}
namespace ERR_LocationKind{enum{PRISON,RADIO,EXTRACTION,OUTPOST,DEPOT,MORTAR,HOSPITAL};}
struct ERR_LocationComponent{string id;int kind=ERR_LocationKind::OUTPOST,knowledge=0;bool prepared=true;string Id(){return id;}int Kind(){return kind;}int Knowledge(){return knowledge;}bool Prepared(){return prepared;}bool Discover(int roll,int chance){if(!prepared||knowledge>=2)return false;knowledge=knowledge==1||roll<chance?2:1;return true;}};
struct Definition{bool RevealOnEscape=false;};
struct ERR_SiteRuntime{struct Definition*Definition=nullptr;};
struct ERR_SiteService{std::map<string,ERR_SiteRuntime*>sites;ERR_SiteRuntime*Find(string id){return sites.count(id)?sites[id]:nullptr;}};
struct ERR_DocumentComponent{string target;string TargetId(){return target;}};
struct Entity;using IEntity=Entity*;
struct InventoryStorageManagerComponent{Array<IEntity>folders;};
struct Entity{bool alive=true,current=true;int faction=1;InventoryStorageManagerComponent*inventory=nullptr;ERR_DocumentComponent*document=nullptr;};
namespace ERR_Actors{bool Alive(IEntity p){return p&&p->alive;}}
namespace SCR_FactionManager{int SGetFaction(IEntity p){return p->faction;}}
struct Roster{bool CurrentActor(IEntity p){return p&&p->current;}};
struct Settings{int required=5,chance=100;int Get(string key){assert(key=="radioIntelSiteCount");return required;}int Runtime(string){return chance;}};
struct Mines{bool Recover(IEntity,IEntity){return false;}};
struct PM{int GetPlayerIdFromControlledEntity(IEntity){return 1;}};
struct Game{PM pm;PM*GetPlayerManager(){return &pm;}}game;Game*GetGame(){return &game;}
void PrintFormat(string,int,string,int){}
struct ERR_IssuedIntel{IEntity Folder=nullptr;string SiteId;};
struct ERR_BodyEvidence{Array<ERR_IssuedIntel*>Issued;bool Searched=false;};
struct Service;struct Evidence;
struct ERR_GameMode{inline static ERR_GameMode*current=nullptr;struct Settings settings;struct Evidence*evidence=nullptr;Service*discovery=nullptr;ERR_SiteService sites;Roster roster;Mines mines;Array<ERR_LocationComponent*>locations;bool active=true,preparing=false;int waiting=0,exhausted=0;
static ERR_GameMode*Get(){return current;}struct Settings*Settings(){return &settings;}struct Evidence*Evidence(){return evidence;}Service*Discovery(){return discovery;}ERR_SiteService*Sites(){return &sites;}Roster*Players(){return &roster;}Mines*MineIntel(){return &mines;}int PlayerFaction(){return 1;}bool Active(){return active;}bool WorldPreparing(){return preparing;}void Locations(Array<ERR_LocationComponent*>&out){out=locations;}void ERR_RadioIntelWaiting(){waiting++;}void ERR_IntelExhausted(bool){exhausted++;}
};
struct Evidence{Array<ERR_BodyEvidence*>m_Roster;Map<string,int>m_TargetAssignments;bool m_Stopped=false;string NextTarget();string Target(IEntity);void Retarget(IEntity,string);};
struct Service{ERR_GameMode*m_Mode;int m_RadioIntelRequired=0,m_RadioIntelIdentified=0;bool m_RadioIntelFinalized=false,m_RadioIntelUnlocked=false,m_Stopped=false;Set<IEntity>m_Spent;float m_CleanupNow=0;int cleanups=0;void QueueCleanup(IEntity,float){cleanups++;}void Folders(InventoryStorageManagerComponent*i,Array<IEntity>&out){out=i->folders;}
'''+cpp(gate)+r'''
};
'''+cpp(ev).replace('string NextTarget(', 'string Evidence::NextTarget(').replace('string Target(', 'string Evidence::Target(').replace('void Retarget(', 'void Evidence::Retarget(')+r'''
struct Fixture{ERR_GameMode mode;Evidence evidence;Service service;ERR_LocationComponent sites[9];Definition automatic;ERR_SiteRuntime autoSite;Entity player,folder;InventoryStorageManagerComponent inv;ERR_DocumentComponent doc;ERR_IssuedIntel issued;ERR_BodyEvidence record;
Fixture():service(&mode){ERR_GameMode::current=&mode;Replication::server=true;mode.discovery=&service;mode.evidence=&evidence;player.inventory=&inv;inv.folders={&folder};
for(int i=0;i<9;i++){sites[i].id="site-"+std::to_string(i);mode.locations.Insert(&sites[i]);}
sites[0].kind=ERR_LocationKind::RADIO;sites[1].kind=ERR_LocationKind::PRISON;sites[2].kind=ERR_LocationKind::EXTRACTION;
automatic.RevealOnEscape=true;autoSite.Definition=&automatic;mode.sites.sites[sites[3].id]=&autoSite;
}
void SealRadio(){issued.Folder=&folder;issued.SiteId=sites[0].id;record.Issued={&issued};evidence.m_Roster={&record};}
void FixedRadio(){doc.target=sites[0].id;folder.document=&doc;}
void Identify(int count){for(int i=4;i<4+count;i++)sites[i].knowledge=2;}
};
int main(){
// Default boundary, exclusions and unique IDs (including duplicate observations).
{Fixture f;for(int i=0;i<4;i++)f.sites[i].knowledge=2;f.Identify(4);f.sites[8].knowledge=1;
f.mode.locations.Insert(&f.sites[4]);f.mode.locations.Insert(nullptr);
assert(!f.service.RadioIntelAllowed()&&f.service.m_RadioIntelIdentified==4&&f.service.m_RadioIntelRequired==5);
f.sites[8].knowledge=2;assert(f.service.RadioIntelAllowed());f.mode.locations.Clear();assert(f.service.RadioIntelAllowed());}
// Setting edits are next-round only; fresh service snapshots the saved value.
{Fixture f;f.mode.settings.required=0;assert(!f.service.RadioIntelAllowed());Service next(&f.mode);assert(next.RadioIntelAllowed());}
// Preparation never lowers the requirement; completed sparse world caps it once.
{Fixture f;f.mode.locations={&f.sites[4],&f.sites[5]};f.mode.preparing=true;f.Identify(2);
assert(!f.service.RadioIntelAllowed()&&f.service.m_RadioIntelRequired==5);f.mode.preparing=false;assert(f.service.RadioIntelAllowed()&&f.service.m_RadioIntelRequired==2);}
{Fixture f;f.mode.locations.Clear();f.mode.active=false;assert(!f.service.RadioIntelAllowed());f.mode.active=true;assert(f.service.RadioIntelAllowed()&&f.service.m_RadioIntelRequired==0);}
// Unprepared sites cannot contribute knowledge or increase the final cap.
{Fixture f;f.sites[8].prepared=false;f.Identify(4);assert(f.service.RadioIntelAllowed()&&f.service.m_RadioIntelRequired==4);}
// Ordinary initial assignments and unsealed recovery both avoid radios.
{Fixture f;assert(f.evidence.NextTarget()!=f.sites[0].id);f.service.Recover(&f.player);assert(f.sites[0].knowledge==0&&f.service.m_Spent.Contains(&f.folder)&&f.service.cleanups==1);}
// Sealed radio folders redirect, including searched bodies; existing report
// snapshots stay detached. Updated evidence remains after ordinary cleanup.
{Fixture f;f.SealRadio();f.record.Searched=true;string queued=f.evidence.Target(&f.folder);f.service.Recover(&f.player);
assert(queued==f.sites[0].id&&f.evidence.Target(&f.folder)!=queued&&f.sites[0].knowledge==0);
assert(f.evidence.Target(&f.folder)==f.sites[3].id&&f.evidence.m_TargetAssignments[f.sites[3].id]==1);}
// A fixed document stays unspent through four sites and processes automatically
// on the next ordinary inventory scan after the fifth becomes fully known.
{Fixture f;f.FixedRadio();f.Identify(4);f.service.Recover(&f.player);f.service.Recover(&f.player);
assert(f.service.m_Spent.empty()&&f.service.cleanups==0&&f.sites[0].knowledge==0);
f.sites[8].knowledge=2;f.service.Recover(&f.player);assert(f.service.m_Spent.Contains(&f.folder)&&f.sites[0].knowledge==2&&f.service.cleanups==0);}
// If there are no alternatives yet, keep ordinary folders and issued targets.
{Fixture f;f.SealRadio();f.mode.preparing=true;f.mode.locations={&f.sites[0]};f.service.Recover(&f.player);
assert(f.service.m_Spent.empty()&&f.service.cleanups==0&&f.mode.exhausted==0&&f.evidence.Target(&f.folder)==f.sites[0].id);
assert(f.evidence.NextTarget().IsEmpty());}
// A redirected rough clue counts only on full identification, never on reuse.
{Fixture f;f.SealRadio();f.mode.settings.chance=0;f.sites[3].knowledge=2;f.service.Recover(&f.player);
assert(f.sites[4].knowledge==1&&f.evidence.Target(&f.folder)==f.sites[4].id);f.service.Recover(&f.player);assert(f.sites[4].knowledge==1);}
// Stopped/client/wrong-owner inventory recovery preserves the authorization gate.
for(int fault=0;fault<4;fault++){Fixture f;if(fault==0)Replication::server=false;if(fault==1)f.service.m_Stopped=true;if(fault==2)f.player.current=false;if(fault==3)f.player.faction=2;f.service.Recover(&f.player);assert(f.service.m_Spent.empty());}
}
''')
print('PASS: radio intel boundary, exclusions, preparation cap, next-round configuration, sticky unlock, redirection/evidence, fixed retention, automatic recovery and lifecycle guards')