Files
EscapistsReintroduced/tests/test_aircraft_field_rules.py
T
jakub cab998fb64 Combine Workshop addon and fix prison startup
Merge owned systems into one addon while preserving resource identities. Gate player admission on the replicated prison scene, restore the original framed door, and hold enemy AI until escape. Correct scenario branding and author metadata, include publishing assets, and retain regression and native verification evidence.
2026-09-20 16:42:52 +02:00

104 lines
6.2 KiB
Python

"""Execute the actual field policy equations; no claim about native stopping/flight."""
from policy_harness import ROOT, run
import re
source = (ROOT/'addons/core/Scripts/Game/ERR/10_AI/ERR_AircraftFieldRules.c').read_text()
cpp = source.replace('class ERR_AircraftFieldRules', 'struct Rules').replace('out vector nextIntegral', 'vector& nextIntegral')
cpp = cpp.replace('Math.', 'Math::').replace('vector.', 'vector::').replace('"0 0 0"', 'vector{}')
cpp = re.sub(r'(?<![\w.])(\d+\.\d+)(?![\w.])', r'\1f', cpp)
cpp = cpp.rstrip() + ';'
run(r'''#include <cassert>
#include <cmath>
#include <algorithm>
#include <initializer_list>
struct vector {
float v[3]={0,0,0};
vector()=default; vector(float x,float y,float z):v{x,y,z}{}
float& operator[](int i){return v[i];} float operator[](int i)const{return v[i];}
vector operator+(vector b)const{return {v[0]+b[0],v[1]+b[1],v[2]+b[2]};}
vector operator-(vector b)const{return {v[0]-b[0],v[1]-b[1],v[2]-b[2]};}
vector operator*(float s)const{return {v[0]*s,v[1]*s,v[2]*s};}
float Length()const{return std::sqrt(Dot(*this,*this));}
static float Dot(vector a,vector b){return a[0]*b[0]+a[1]*b[1]+a[2]*b[2];}
static float DistanceXZ(vector a,vector b){return std::hypot(a[0]-b[0],a[2]-b[2]);}
};
struct Math {static float AbsFloat(float x){return std::abs(x);} static float Clamp(float x,float lo,float hi){return std::clamp(x,lo,hi);} static float Max(float a,float b){return std::max(a,b);} static float Min(float a,float b){return std::min(a,b);} static float Sqrt(float a){return std::sqrt(a);}};
'''+cpp+r'''
bool close(float a,float b){return std::abs(a-b)<.00001f;}
int main(){
assert(Rules::Length(99.95f)&&Rules::Length(100.05f));
assert(!Rules::Length(99.949f)&&!Rules::Length(100.051f)&&!Rules::Length(NAN));
assert(Rules::SpeedAllowed({1.5f,9,2})&&!Rules::SpeedAllowed({1.501f,0,2}));
assert(!Rules::SpeedAllowed({0,NAN,0})&&!Rules::SpeedAllowed({INFINITY,0,0}));
assert(Rules::ValidStep(.5f)&&!Rules::ValidStep(0)&&!Rules::ValidStep(.501f)&&!Rules::ValidStep(NAN));
float speed=0;
for(int i=0;i<100;i++){float next=Rules::RampSpeed(speed,2,.1f);assert(next-speed<=.020001f);speed=next;}
assert(close(speed,2));
for(int i=0;i<100;i++){float next=Rules::RampSpeed(speed,0,.1f);assert(next<=speed&&speed-next<=.020001f);speed=next;}
assert(close(speed,0));
assert(close(Rules::RampSpeed(1,2,1),1)&&close(Rules::RampSpeed(1,NAN,.1f),1));
vector previous={2,0,0};
for(int i=0;i<400;i++){
vector ramped=Rules::RampReference(previous,{-2,8,0},.1f);
assert((ramped-previous).Length()<=.020001f&&ramped.Length()<=2.00001f&&ramped[1]==0);
previous=ramped;
}
assert(previous[0]<-1.99f);
vector sideways=Rules::RampReference({2,0,0},{0,0,2},.1f);
assert((sideways-vector{2,0,0}).Length()<=.020001f);
assert(close(Rules::RampReference({1,4,0},{-2,0,0},0)[0],1));
assert(!Rules::ShouldBrake(100.23f,1)&&!Rules::ShouldBrake(108,2)&&!Rules::ShouldBrake(120,2));
assert(Rules::ShouldBrake(120.01f,1)&&!Rules::Length(108));
vector integral, next;
for(int i=0;i<2000;i++){
vector demand=Rules::VelocityDemand({2,100,0},{0,-2,0},integral,.1f,next);
assert(close(demand[0],.08f)&&demand[1]==0&&next.Length()==0);integral=next;
}
// Low error accumulates useful demand rather than permanently freezing I.
integral={};vector demand=Rules::VelocityDemand({.1f,5,0},{0,7,0},integral,.5f,next);
assert(close(next[0],.05f)&&close(demand[0],.011f)&&demand[1]==0);
// Opposing error unwinds a positive stored integral.
integral={4,0,0};demand=Rules::VelocityDemand({0,0,0},{.1f,0,0},integral,.5f,next);
assert(next[0]<4&&demand[0]<.08f);
// Diagonal demand is capped as a vector, not independently on both axes.
demand=Rules::VelocityDemand({2,3,2},{-1,4,-1},{4,5,4},.1f,next);
assert(demand.Length()<=.080001f&&demand[1]==0&&next.Length()<=4.00001f&&next[1]==0);
demand=Rules::VelocityDemand({0,0,0},{2,0,0},{},.1f,next);
assert(demand[0]<0&&close(demand.Length(),.08f));
demand=Rules::VelocityDemand({NAN,0,0},{},{1,0,0},.1f,next);assert(demand.Length()==0&&next.Length()==0);
demand=Rules::VelocityDemand({1,0,0},{},{1,0,0},0,next);assert(demand.Length()==0&&next.Length()==0);
assert(close(Rules::BrakingReserve(0),3)&&close(Rules::BrakingReserve(2.5f),26.333334f));
assert(Rules::ShouldBrake(3,0)&&!Rules::ShouldBrake(3.001f,0)&&!Rules::ShouldBrake(25,0));
for(float actualSpeed:{0.0f,.1f,.25f,1.0f,1.5f,2.0f,2.5f}){
float reserve=Rules::BrakingReserve(actualSpeed);
assert(Rules::ShouldBrake(reserve,actualSpeed)&&!Rules::ShouldBrake(reserve+.001f,actualSpeed));
}
// Stop reference never chases the destination or reverses direction.
for(vector initial:{vector{2,0,0},vector{-1.2f,0,1.6f},vector{0,0,0}}){
vector v=initial;
for(int i=0;i<110;i++){
vector nextV=Rules::RampReference(v,{},.1f);
assert(nextV.Length()<=v.Length()+.000001f&&(nextV-v).Length()<=.020001f);
assert(vector::Dot(nextV,initial)>=-.000001f);v=nextV;
}
assert(v.Length()<=.00001f);
}
assert(Rules::ShouldBrake(26,2.5f)&&!Rules::ShouldBrake(27,2.5f));
assert(Rules::ShouldBrake(50,NAN)&&Rules::ShouldBrake(NAN,1));
assert(Rules::LeaseAdmitted(1000,241000)&&Rules::LeaseAdmitted(1000,240000));
assert(!Rules::LeaseAdmitted(1000,239999)&&!Rules::LeaseAdmitted(1000,241001)&&!Rules::LeaseAdmitted(-1,239999));
assert(!Rules::AbortDue(154999,240000)&&Rules::AbortDue(155000,240000));
assert(Rules::AbortDue(240000,240000)&&Rules::AbortDue(NAN,240000));
assert(Rules::LowSpeedHeld(.249f,1000,3000)&&!Rules::LowSpeedHeld(.25f,1000,3000));
assert(!Rules::LowSpeedHeld(0,1000,2999)&&!Rules::LowSpeedHeld(0,-1,3000)&&!Rules::LowSpeedHeld(NAN,0,3000));
vector start={0,10,0},end={100,10,0};
assert(Rules::Contains(start,end,{50,18,7.999f},1));
assert(!Rules::Contains(start,end,{50,18,8},1));
assert(Rules::Contains(start,end,{-7.99f,18,0},1)&&!Rules::Contains(start,end,{-8,18,0},1));
assert(!Rules::Contains(start,end,{108,18,0},1)&&!Rules::Contains(start,end,{50,28.01f,0},1));
assert(!Rules::Contains(start,end,{50,6.99f,0},1)&&!Rules::Contains(start,end,{50,18,0},.84f));
assert(!Rules::Contains(start,end,{NAN,18,0},1)&&!Rules::Contains(start,end,{50,18,0},NAN));
}
''')
print('PASS: actual field PI/ramp/vector caps, antiwindup, braking hypothesis, immutable admission limits and segment bounds; no native physics tested')