Files
EscapistsReintroduced/tests/test_mortar_aim_renewal.py
T

84 lines
6.4 KiB
Python

"""Execute actual aim-equivalence and renewal control flow with synthetic API data.
No fake native ballistic result is represented as a measured firing result.
"""
import re
from policy_harness import ROOT, body, run
r=ROOT/'addons/ai/Scripts/Game/ERR'
solution=r/'ERR_MortarAimSolution.c'; firing=r/'ERR_MortarFiring.c'
eq=body(solution,'static bool Equivalent(').replace('vector.Distance','vector::Distance').replace('Math.AbsFloat','std::fabs')
matches=body(solution,'bool Matches(').replace('ERR_MortarBattery battery','ERR_MortarBattery* battery').replace('ERR_HQReport report','ERR_HQReport* report').replace('battery.','battery->')
fresh=body(r/'ERR_MortarPrediction.c','static bool Fresh(').replace('ERR_HQReport report','ERR_HQReport* report').replace('report.','report->')
renew=body(firing,'bool ERR_RevalidateAim(')
for typ in ['ERR_MortarBattery','ERR_MortarSystem','ERR_HQReport','ERR_MortarAimSolution']:
renew=re.sub(r'\b'+typ+r' (battery|system|report|commanded|updated)\b',typ+r'* \1',renew)
renew=renew.replace('IEntity gun;','IEntity gun=nullptr;').replace('ResourceName ammo;','std::string ammo;')
for name in ['battery','system','report','commanded','updated']:renew=renew.replace(name+'.',name+'->')
for name in ['ERR_MortarBattery','ERR_MortarAimRules','ERR_MortarPrediction']:renew=renew.replace(name+'.',name+'::')
# Native trace formatting is deliberately inert. Authorization branches remain
# the production method body, and every solver invocation is counted below.
renew=re.sub(r', string\.Format\([^\n]*\)\);',');',renew)
run('''#include <cassert>
#include <cmath>
#include <limits>
#include <string>
struct vector {float x=0,y=0,z=0; static float Distance(vector a,vector b){return std::sqrt((a.x-b.x)*(a.x-b.x)+(a.y-b.y)*(a.y-b.y)+(a.z-b.z)*(a.z-b.z));}};
using IEntity=void*;
struct ERR_HQReport {IEntity Target=this;bool Distress=false;float ObservedAt=0;std::string TravelMode="foot";};
struct ERR_MortarPrediction {static constexpr float MAX_REPORT_AGE_MS=60000;
'''+fresh+'''};
struct ERR_MortarAimRules {
'''+eq+'''};
struct Node;struct ERR_MortarAimSolution;
struct ERR_MortarBattery {static inline std::string SHELL="nativeHE";IEntity Gun=this;ERR_MortarAimSolution* AimSolution=nullptr;Node* AimSolver=nullptr;int OrderId=1,OrderShots=0;float AimedAt=1000; void Trace(const char*,const char*){}};
struct ERR_MortarAimSolution {IEntity Gun=nullptr;ERR_HQReport* Report=nullptr;int OrderId=1,Shot=0,Charge=0;vector GunOrigin{},Impact{500,0,0},AimTarget{500,500,0};float FlightTime=30;
'''+matches+'''};
struct ERR_MortarSystem {int rejects=0;bool Withhold(ERR_MortarBattery*,const char*){rejects++;return false;}const char* ContactFailure(ERR_MortarBattery*){return "stale";}};
struct Node {
static constexpr int PORT_ARTILLERY_ENTITY=1,PORT_AMMO_PREFAB=2;
IEntity portGun=nullptr;std::string portAmmo=ERR_MortarBattery::SHELL;ERR_MortarAimSolution* calculated=nullptr;int solves=0;
void GetVariableIn(int,IEntity& out){out=portGun;}void GetVariableIn(int,std::string& out){out=portAmmo;}
ERR_MortarAimSolution* ERR_Solve(ERR_MortarBattery*,ERR_MortarSystem* system,ERR_HQReport*,float){solves++;if(!calculated)system->rejects++;return calculated;}
'''+renew+'''};
struct Fixture {
ERR_MortarBattery battery;ERR_HQReport report;ERR_MortarAimSolution original,next;ERR_MortarSystem system;Node node;
Fixture(){original.Gun=battery.Gun;original.Report=&report;next=original;battery.AimSolution=&original;battery.AimSolver=&node;node.portGun=battery.Gun;node.calculated=&next;}
bool attempt(float now=25000){return node.ERR_RevalidateAim(&battery,&system,&report,now);}
};
int main(){
Fixture stable; assert(stable.attempt()); assert(stable.node.solves==1&&stable.battery.AimedAt==25000);
assert(stable.report.ObservedAt==0&&stable.battery.AimSolution==&stable.original);
assert(stable.attempt(45000)&&stable.node.solves==2); // fresh recomputation every renewal
assert(!stable.attempt(60001)&&stable.node.solves==2&&stable.battery.AimedAt==45000); // observation never renewed
Fixture limit;assert(limit.attempt(60000));
Fixture future;future.report.ObservedAt=26000;assert(!future.attempt()&&future.node.solves==0);
Fixture gone;gone.battery.AimSolver=nullptr;assert(!gone.attempt()&&gone.node.solves==0);
Fixture wrongGun;wrongGun.node.portGun=&wrongGun.report;assert(!wrongGun.attempt()&&wrongGun.node.solves==0);
Fixture wrongAmmo;wrongAmmo.node.portAmmo="other";assert(!wrongAmmo.attempt()&&wrongAmmo.node.solves==0);
Fixture newReport;ERR_HQReport different;newReport.original.Report=&different;assert(!newReport.attempt()&&newReport.node.solves==0);
Fixture nextOrder;nextOrder.battery.OrderId++;assert(!nextOrder.attempt()&&nextOrder.node.solves==0);
Fixture nextShot;nextShot.battery.OrderShots++;assert(!nextShot.attempt()&&nextShot.node.solves==0);
Fixture solverFail;solverFail.node.calculated=nullptr;assert(!solverFail.attempt()&&solverFail.battery.AimedAt==1000);
Fixture charge;charge.next.Charge++;assert(!charge.attempt()&&charge.battery.AimedAt==1000);
Fixture gunMove;gunMove.next.GunOrigin.y=.051f;assert(!gunMove.attempt());
Fixture impactMove;impactMove.next.Impact.y=.101f;assert(!impactMove.attempt());
Fixture aimMove;aimMove.next.AimTarget.y+=.101f;assert(!aimMove.attempt());
Fixture flight;flight.next.FlightTime+=.051f;assert(!flight.attempt());
Fixture invalid;invalid.next.FlightTime=std::numeric_limits<float>::quiet_NaN();assert(!invalid.attempt());
Fixture slowDrift;slowDrift.next.Impact.x+=.06f;assert(slowDrift.attempt());
slowDrift.next.Impact.x+=.06f;assert(!slowDrift.attempt(35000)); // compare original, not last accepted approximation
assert(slowDrift.battery.AimSolution==&slowDrift.original&&slowDrift.original.Impact.x==500);
}
''')
# The tested renewal is used at both existing pre-shot and physical-transfer
# gates; it may neither emit a replacement barrel command nor change a report.
source=firing.read_text();system=(r/'ERR_MortarSystem.c').read_text()
assert source.count('system.AllowShot(battery)')==2
assert 'SetVariableOut' not in body(firing,'bool ERR_RevalidateAim(')
assert 'ObservedAt =' not in source
assert 'now - battery.AimedAt > 10000' in system
assert 'battery.AimSolver.ERR_RevalidateAim(battery, this, report, now)' in system
assert source.count('BallisticTable.GetAimHeightOfProjectileAltitudeFromSource(')==1
assert 'GetAmmoInitialSpeedCoefficients(ammo, coefficients)' in body(firing,'protected ERR_MortarAimSolution ERR_Solve(')
print('PASS: actual aim renewal requires fresh same-report solve and original-command equivalence; native firing untested')