Files
EscapistsReintroduced/tests/test_gameplay_session_regressions.py

111 lines
8.8 KiB
Python

"""Execute production gates behind the RKS2 delayed-start and hunter failures."""
import re
from policy_harness import ROOT, body, run
r = ROOT/'addons/core/Scripts/Game/ERR/10_AI'
# Use the actual stateful deadline block, including first-poll initialization.
poll = body(r/'ERR_MortarReadiness.c', 'ERR_MortarSupplyState Poll(')
clock = poll[poll.index(' if (m_StartedAt < 0)'):poll.index(' array<AIAgent> agents')]
admission = body(r/'ERR_HQ.c', 'protected bool ConvoyAdmissionReady(').replace('protected ', '')
admission = admission.replace('array<ref ERR_MortarBattery> batteries = {};', 'Array<ERR_MortarBattery*> batteries;')
admission = admission.replace('foreach (', 'for (').replace('ERR_HQUnit unit', 'ERR_HQUnit* unit').replace('ERR_MortarBattery support', 'ERR_MortarBattery* support').replace('ERR_MortarBattery battery', 'ERR_MortarBattery* battery')
for name in ('m_Mode', 'm_Mortars', 'unit', 'support', 'battery'):
admission = admission.replace(name+'.', name+'->')
admission = admission.replace('Forces().', 'Forces()->')
crew = body(r/'ERR_HunterService.c', 'override bool CrewValid(').replace('override ', '').replace('ERR_GameMode mode','Mode* mode')
crew = crew.replace('ERR_Actors.', 'Actors::').replace('mode.', 'mode->').replace('Group.', 'Group->').replace('Seats[0].','Seats[0]->')
run(r'''
#include <cassert>
#include <vector>
#include <algorithm>
struct Text { bool empty=true; bool IsEmpty(){return empty;} };
template<class T>struct Array:std::vector<T>{using std::vector<T>::vector;bool IsEmpty(){return this->empty();}int Count(){return this->size();}bool Contains(T x){return std::find(this->begin(),this->end(),x)!=this->end();}};
struct GroupType {int faction=1;int GetFaction(){return faction;}};
struct Actor {bool alive=true,fit=true,player=false;GroupType* group=nullptr;};
struct Seat {Actor* occupant=nullptr;Actor* GetOccupant(){return occupant;}};
namespace Actors {bool Alive(Actor* x){return x&&x->alive;}bool CanFight(Actor* x){return x&&x->alive&&x->fit;}bool Player(Actor*x){return x&&x->player;}GroupType* Group(Actor*x){return x?x->group:nullptr;}}
struct Clock {float m_StartedAt=-1; bool Poll(float now){
'''+clock+r'''
return expired;
}};
struct Ledger {std::vector<GroupType*> registered;bool FindGroup(GroupType*g){return std::find(registered.begin(),registered.end(),g)!=registered.end();}};
struct Mode {Ledger ledger;Ledger* Forces(){return &ledger;}int EnemyFaction(){return 1;}};
struct ERR_HQUnit {GroupType* Group=nullptr;};
struct ERR_MortarBattery {GroupType* group=nullptr;bool stopped=false,StartupActivated=false;Text SpawnFailure;bool Stopped(){return stopped;}};
struct Mortars {Array<ERR_MortarBattery*> all;ERR_MortarBattery* FindGroup(GroupType*g){for(auto*b:all)if(b->group==g)return b;return nullptr;}void Batteries(Array<ERR_MortarBattery*>&out){out=all;}};
struct HQ {Array<ERR_HQUnit*> m_Units;Mode* m_Mode;Mortars* m_Mortars;
'''+admission+r'''
};
struct Hunter {GroupType* Group;Actor* Entity;Actor* Driver;Array<Actor*> Members;Array<Seat*> Seats;
'''+crew+r'''
};
int main(){
Clock c;assert(!c.Poll(150000));assert(!c.Poll(239999));assert(c.Poll(240000)); // 150 seconds in prison does not spend the initialization budget
Clock early;assert(!early.Poll(1000));assert(!early.Poll(90999));assert(early.Poll(91000));assert(early.Poll(999)); // finite deadline and clock reversal remain enforced
Mode mode;GroupType ordinary,mortar;ERR_HQUnit u{&ordinary},m{&mortar};ERR_MortarBattery battery{&mortar};Mortars support;support.all={&battery};HQ hq{{&u,&m},&mode,&support};
mode.ledger.registered={&ordinary};assert(!hq.ConvoyAdmissionReady()); // unfinished mortar waits
battery.SpawnFailure.empty=false;assert(hq.ConvoyAdmissionReady()); // failed optional mortar cannot block convoy admission
battery.SpawnFailure.empty=true;battery.stopped=true;assert(hq.ConvoyAdmissionReady());
battery.stopped=false;battery.StartupActivated=true;assert(!hq.ConvoyAdmissionReady()); // success still requires ledger registration
mode.ledger.registered.push_back(&mortar);assert(hq.ConvoyAdmissionReady());
mode.ledger.registered={&mortar};battery.SpawnFailure.empty=false;assert(!hq.ConvoyAdmissionReady()); // unrelated pending group is not bypassed
Actor vehicle,driver,gunner;driver.group=&ordinary;gunner.group=&ordinary;Seat seat{&driver};Hunter hunter{&ordinary,&vehicle,&driver,{&driver,&gunner},{&seat}};
assert(hunter.CrewValid(&mode));gunner.alive=false;assert(hunter.CrewValid(&mode)); // loss of gunner does not discard a working driver
driver.fit=false;assert(!hunter.CrewValid(&mode));driver.fit=true;assert(hunter.CrewValid(&mode)); // temporary incapacitation can resume
seat.occupant=nullptr;assert(!hunter.CrewValid(&mode));seat.occupant=&driver;assert(hunter.CrewValid(&mode));
driver.player=true;assert(!hunter.CrewValid(&mode));driver.player=false;vehicle.alive=false;assert(!hunter.CrewValid(&mode));
}
''')
print('PASS: real-session mortar deadline, failed-support convoy admission and hunter survivor/recovery gates')
# Execute the real retry method with a newer order still pending: the physical
# waypoint, not the unsent destination, must be recreated. Exercise retry limits
# and combat suppression too.
movement = body(r/'ERR_HunterService.c', 'protected void ObserveMovement(')
movement = movement.replace('protected ', '').replace('ERR_HunterVehicle unit', 'MovingHunter* unit')
movement = movement.replace('SCR_AIGroupUtilityComponent utility', 'Utility* utility').replace('vector ', 'Vec ').replace('vector.DistanceXZ', 'Vec::DistanceXZ')
movement = movement.replace('AIWaypoint.Cast(ERR_Actors.Spawn(ERR_Resources.MoveWaypoint(), retryDestination))', 'Spawn(retryDestination)')
for name in ('unit', 'utility'):
movement = movement.replace(name+'.', name+'->')
for name in ('Entity', 'Group', 'Waypoint', 'm_Perception'):
movement = movement.replace(name+'.', name+'->')
movement = movement.replace('Physics physics =', 'Physics* physics =').replace('physics.GetVelocity()', 'physics->GetVelocity()')
movement = movement.replace('GetPhysics().', 'GetPhysics()->').replace('unit->Repaths.ToString()', 'std::to_string(unit->Repaths)')
movement = movement.replace('string.Format', 'Format').replace('ERR_HQ.Event', 'Event').replace('ERR_HunterCombat.Active', 'ERR_HunterCombat::Active')
movement = re.sub(r'\b(bool|float|Vec) (\w+);', r'\1 \2 {};', movement)
run(r'''
#include <cassert>
#include <cmath>
#include <string>
struct Vec {float x=0,z=0;float Length(){return 0;}static float DistanceXZ(Vec a,Vec b){return std::hypot(a.x-b.x,a.z-b.z);}};
struct Targets {bool empty=true;bool IsEmpty(){return empty;}};
struct Perception {Targets m_aTargets;};
struct Utility {Perception* m_Perception;};
struct Physics {Vec GetVelocity(){return {};}};
struct Vehicle {Vec pos;Physics physics;Vec GetOrigin(){return pos;}Physics* GetPhysics(){return &physics;}};
struct Group {Utility utility;Utility* GetGroupUtilityComponent(){return &utility;}void AddWaypoint(void*){}};
namespace ERR_HunterCombat {bool fresh=false;bool Active(Group*){return fresh;}};
struct Waypoint {Vec pos;bool arrived=false;bool IsWithinCompletionRadius(Group*){return arrived;}Vec GetOrigin(){return pos;}void SetCompletionRadius(float){}};
struct MovingHunter {Vehicle* Entity;struct Group* Group;struct Waypoint* Waypoint;Vec Destination,ProgressPosition;float ProgressAt=1,NextMotionLog=0;int Repaths=0;std::string Source="test";void ClearOrder(){Waypoint=nullptr;}};
// Actual abandonment/exit behavior is covered by test_hunter_stuck.py.
int abandonments=0;void AbandonStuckVehicle(MovingHunter*){abandonments++;}
Waypoint spawned;Waypoint* Spawn(Vec pos){spawned.pos=pos;return &spawned;}
template<class... Args> std::string Format(Args...){return "log";}
void Event(std::string,std::string){}
'''+movement+r'''
int main(){
Perception perception;Group group{{&perception}};Vehicle vehicle;Waypoint delivered{{100,200}};
MovingHunter hunter{&vehicle,&group,&delivered,{900,900}};
ObserveMovement(&hunter,60000);assert(hunter.Waypoint==&delivered&&hunter.Repaths==0);
ObserveMovement(&hunter,60001);assert(hunter.Repaths==1&&hunter.Waypoint->pos.x==100&&hunter.Waypoint->pos.z==200);assert(hunter.Destination.x==900);
ObserveMovement(&hunter,120001);ObserveMovement(&hunter,180001);assert(hunter.Repaths==3);
ObserveMovement(&hunter,240001);assert(hunter.Repaths==3&&abandonments==1);
// A separate still-owned patrol makes progress and resumes after contact.
hunter={&vehicle,&group,&delivered,{900,900}};
vehicle.pos={20,0};ObserveMovement(&hunter,241001);assert(hunter.Repaths==0);
perception.m_aTargets.empty=false;ERR_HunterCombat::fresh=true;ObserveMovement(&hunter,400000);assert(hunter.Repaths==0);
ERR_HunterCombat::fresh=false;ObserveMovement(&hunter,400001);assert(hunter.Repaths==0);
ObserveMovement(&hunter,460000);assert(hunter.Repaths==1);
}
''')
print('PASS: native-route retry preserves delivered target, limits attempts and suspends during combat')