Files
EscapistsReintroduced/tests/test_hq_tactical_ground.py
T
jakub 6a667d8deb Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
2026-10-13 22:52:53 +02:00

225 lines
14 KiB
Python

"""Execute the actual local terrain selector against deterministic native boundary doubles."""
from policy_harness import ROOT, body, run
import re
P = ROOT / 'addons/core/Scripts/Game/ERR/10_AI/ERR_HQTacticalGround.c'
A = ROOT / 'addons/core/Scripts/Game/ERR/10_AI/ERR_HQCoordinated.c'
def cpp(s):
s = s.replace('protected static float s_Window', 'inline static float s_Window')
s = s.replace('protected static int s_Samples', 'inline static int s_Samples')
s = s.replace('protected ', '').replace('out vector ', 'vector& ')
s = s.replace('class ERR_HQTacticalGround', 'struct ERR_HQTacticalGround')
for name in ('ERR_HQMemoryRules', 'ERR_HQTacticalGroundRules', 'ERR_HQOperationRole',
'ERR_Actors', 'ChimeraWorldUtils', 'TraceFlags', 'Math', 'vector'):
s = s.replace(name + '.', name + '::')
s = s.replace('AIPathfindingComponent.Cast(group.FindComponent(AIPathfindingComponent))',
'group->path')
s = s.replace('ChimeraCharacter.Cast(parent)', 'parent->character')
s = s.replace('Vehicle.Cast(parent)', 'parent->vehicle')
for kind in ('IEntity', 'Physics', 'TraceParam', 'TraceBox', 'SCR_AIGroup',
'ERR_HQTask', 'ERR_AttackSpace', 'AIPathfindingComponent',
'NavmeshWorldComponent', 'ERR_HQUnit', 'ERR_HQWatchEntry',
'ERR_HQBelief', 'ERR_HQOperationSlot', 'ERR_HQTacticalGround'):
s = re.sub(r'\b' + kind + r' (\w+)', kind + r'* \1', s)
for name in ('parent', 'physics', 'trace', 'volume', 'path', 'navmesh', 'task',
'occupied', 'unit', 'own', 'belief', 'slot', 'Report', 'Task', 'survey'):
s = s.replace(name + '.', name + '->')
s = s.replace('GetGame().GetWorld().', 'GetGame()->GetWorld()->')
s = s.replace('GetGame().GetWorld()', 'GetGame()->GetWorld()')
s = s.replace('TraceMove(trace, StaticObstacle)',
'TraceMove(trace, [this](IEntity* e){return StaticObstacle(e);})')
s = s.replace('TraceMove(volume, StaticObstacle)',
'TraceMove(volume, [this](IEntity* e){return StaticObstacle(e);})')
s = re.sub(r'"(-?[0-9.]+) (-?[0-9.]+) (-?[0-9.]+)"', r'Vector(\1,\2,\3)', s)
s = re.sub(r'\bbool (found|sampled|standingClear|lowCovered|approachCovered);', r'bool \1=false;', s)
s = s.replace('int requested;', 'int requested=0;').replace('float bestScore;', 'float bestScore=0;')
return s
rules = cpp(body(P, 'class ERR_HQTacticalGroundRules')) + ';'
selector = cpp(body(P, 'class ERR_HQTacticalGround\n')) + ';'
adapter = cpp(body(A, 'protected bool CoordinatedGround(')).replace(', m_AttackSpace,', ', &m_AttackSpace,')
run(r'''
#include <cassert>
#include <cmath>
#include <algorithm>
#include <functional>
#include <limits>
#include <string>
using string=std::string;
struct vector {
float x=0,y=0,z=0;float& operator[](int i){return i==0?x:i==1?y:z;}
vector operator+(vector b)const{return{x+b.x,y+b.y,z+b.z};}
vector operator-(vector b)const{return{x-b.x,y-b.y,z-b.z};}
vector operator*(float n)const{return{x*n,y*n,z*n};}
float Length()const{return std::sqrt(x*x+y*y+z*z);}
vector Normalized()const{return *this*(1/Length());}
void Normalize(){*this=Normalized();}
static float DistanceXZ(vector a,vector b){return std::hypot(a.x-b.x,a.z-b.z);}
};
vector Vector(float x,float y,float z){return{x,y,z};}
struct Math{static float AbsFloat(float v){return std::abs(v);}static float Min(float a,float b){return std::min(a,b);}};
struct ERR_HQMemoryRules{
static bool Finite(float v){return std::isfinite(v)&&std::abs(v)<=1e12;}
static bool PositionValid(vector p){return Finite(p.x)&&Finite(p.y)&&Finite(p.z)&&std::abs(p.x)<=1e6&&std::abs(p.y)<=1e6&&std::abs(p.z)<=1e6;}
static bool DeliveryFresh(float at,float now){return Finite(at)&&Finite(now)&&at>=0&&now>=at&&now-at<=120000;}
};
enum class ERR_HQOperationRole{OBSERVE,FIX,FLANK,BLOCK,SEARCH,RESERVE,WITHDRAW};
struct ERR_HQTask{ERR_HQOperationRole OperationRole=ERR_HQOperationRole::OBSERVE;vector Destination{100,.1,100},SupportPosition{100,.1,0};float SupportUntil=30000;int OperationId=1,BeliefId=1,BeliefRevision=2,ExecutionId=1;};
struct Physics{bool dynamic=false;bool IsDynamic(){return dynamic;}};
struct IEntity{bool character=false,vehicle=false;Physics*physics=nullptr;IEntity*parent=nullptr;Physics*GetPhysics(){return physics;}IEntity*GetParent(){return parent;}};
struct TraceParam{vector Start,End;int Flags=0;};
struct TraceBox:TraceParam{vector Mins,Maxs;};
struct TraceFlags{enum{WORLD=1,ENTS=2};};
struct NativeWorld{
int scene=0,rays=0,boxes=0;bool cliff=false,water=false;
float Surface(vector p){return cliff&&p.x>100?4.1:.1;}
float TraceMove(TraceBox*,const std::function<bool(IEntity*)>&){boxes++;return 1;}
float TraceMove(TraceParam*t,const std::function<bool(IEntity*)>&){
rays++;
if(scene==3)return std::numeric_limits<float>::quiet_NaN();
if(scene==4)return 0;
if(scene==1||scene==2){
if(t->Start.z<84&&std::abs(t->Start.x-100)<1)return .01; // blind wall north
if(t->Start.x<84&&std::abs(t->Start.z-100)<13){
if(t->Start.y<1.4)return .025; // low wall west with clear standing view
}
}
return 1;
}
} world;
struct Game{NativeWorld*GetWorld(){return &world;}} game;
Game*GetGame(){return &game;}
struct ERR_Actors{static vector Ground(vector p){p.y=world.Surface(p);return p;}};
struct ChimeraWorldUtils{static bool TryGetWaterSurfaceSimple(NativeWorld*,vector p){return world.water&&p.x>101;}};
struct NavmeshWorldComponent{
bool loaded=true;int requests=0;bool IsTileLoaded(vector){return loaded;}
bool IsTileRequested(vector){return false;}void LoadTileIn(vector){requests++;}
};
struct AIPathfindingComponent{
bool allow=true;vector delta;NavmeshWorldComponent nav;int projections=0;
NavmeshWorldComponent*GetNavmeshComponent(){return &nav;}
bool GetClosestPositionOnNavmesh(vector p,vector,vector&out){projections++;out=p+delta;return allow;}
};
struct SCR_AIGroup{AIPathfindingComponent*path=nullptr;};
struct ERR_AttackSpace{bool allow=true;int checks=0;bool Free(SCR_AIGroup*,vector,float){checks++;return allow;}};
'''+rules+selector+r'''
struct ERR_HQWatchEntry{bool Closed=false;int Fit=4;float ObservedAt=1000;vector Position{100,.1,200};};
struct ERR_HQBelief{int BeliefRevision=2;vector Position{100,.1,0};};
struct ERR_HQExecutionReport{int ExecutionId=1;float ObservedAt=2000;vector Position{100,.1,100};};
struct ERR_HQOperationSlot{ERR_HQExecutionReport*Report=nullptr;};
struct ERR_HQUnit{SCR_AIGroup*Group=nullptr;void*Utility=this;bool HomeGuard=false,ReservedForSupport=false;int LogisticsReservation=0,Id=1;string ForceSourceId="one";ERR_HQTask*Task=nullptr;};
struct Watch{ERR_HQWatchEntry*entry=nullptr;ERR_HQWatchEntry*Find(string){return entry;}};
struct Memory{ERR_HQBelief*belief=nullptr;ERR_HQBelief*GetById(int){return belief;}};
struct Operations{ERR_HQOperationSlot*slot=nullptr;ERR_HQOperationSlot*Find(int){return slot;}};
struct Adapter{Watch m_Watch;Memory m_Memory;Operations m_Operations;ERR_AttackSpace m_AttackSpace;
'''+adapter+r'''
};
int main(){
using R=ERR_HQTacticalGroundRules;using Role=ERR_HQOperationRole;
vector anchor{100,.1,100},from{100,.1,200},threat{100,.1,0},selected;
float nan=std::numeric_limits<float>::quiet_NaN();
// At an otherwise identical point the observer prefers a usable low wall,
// while a withdrawing squad prefers concealment over a clear firing view.
assert(R::Score(Role::OBSERVE,100,0,0,true,true,true,false)>R::Score(Role::OBSERVE,100,0,0,true,true,false,false));
assert(R::Score(Role::OBSERVE,100,0,0,true,true,false,false)>R::Score(Role::OBSERVE,100,0,0,true,false,true,false));
assert(R::Score(Role::WITHDRAW,100,0,0,true,false,true,true)>R::Score(Role::WITHDRAW,100,0,0,true,true,true,true));
assert(R::Score(Role::FLANK,20,0,0,true,false,true,true)>R::Score(Role::FLANK,20,0,0,true,true,false,false));
assert(R::Score(Role::RESERVE,100,0,0,false,true,true,true)==R::Score(Role::RESERVE,100,0,0,false,false,false,false));
for(Role role:{Role::OBSERVE,Role::FIX,Role::FLANK,Role::BLOCK,Role::SEARCH,Role::RESERVE,Role::WITHDRAW}){
for(int i=0;i<9;i++){vector p=R::Candidate(anchor,threat,role,i);assert(vector::DistanceXZ(anchor,p)<=R::Radius(role));}
}
assert(R::Candidate(anchor,threat,Role::SEARCH,8).x==anchor.x); // no survey of neighbouring search patches
assert(!R::Admissible(Role::SEARCH,anchor,from,{102.01,.1,100},0,1000));
assert(!R::Admissible(Role::OBSERVE,anchor,from,{100,5,100},0,1000));
assert(!R::Admissible(Role::OBSERVE,anchor,from,{nan,0,100},0,1000));
assert(!R::Admissible(Role::FLANK,{120,.1,100},anchor,{120.1,.1,100},30000,1000));
assert(!R::Admissible(Role::FLANK,{120,.1,100},anchor,{120,.1,100},9000,1000));
assert(R::Admissible(Role::FLANK,{120,.1,100},anchor,{120,.1,100},10000,1000));
assert(!R::Admissible(Role::FLANK,{120,.1,100},anchor,{120,.1,100},nan,1000));
ERR_HQTacticalGround survey;AIPathfindingComponent path;SCR_AIGroup group;group.path=&path;
ERR_AttackSpace space;ERR_HQTask task;
world.scene=1;
assert(survey.Select(&group,&task,from,threat,&space,1000,selected));
assert(selected.x<84&&std::abs(selected.z-100)<1); // takes the low wall, not the blind wall or exposed anchor
assert(world.rays<=27&&world.boxes==9);
task.OperationRole=Role::RESERVE;
assert(survey.Select(&group,&task,from,threat,&space,1000,selected));
assert(selected.z<84&&std::abs(selected.x-100)<1); // reserve uses the blind side for protection
task.OperationRole=Role::WITHDRAW;
assert(survey.Select(&group,&task,from,threat,&space,1000,selected));
int geometry=world.rays+world.boxes;
assert(!survey.Select(&group,&task,from,threat,&space,1000,selected));
assert(world.rays+world.boxes==geometry&&geometry<=108); // shared hard work budget
assert(!survey.Select(&group,&task,from,threat,&space,1999,selected));
assert(survey.Select(&group,&task,from,threat,&space,2000,selected));
// Flat open ground retains the original destination. It is still a candidate
// only; no contact/arrival/route claim is returned or manufactured.
world.scene=0;task.OperationRole=Role::OBSERVE;
assert(survey.Select(&group,&task,from,threat,&space,3000,selected));
assert(vector::DistanceXZ(anchor,selected)<.01);
// Tile loading stays asynchronous and bounded; absence never accepts origin.
path.nav.loaded=false;int before=world.rays+world.boxes;
assert(!survey.Select(&group,&task,from,threat,&space,4000,selected));
assert(path.nav.requests==2&&world.rays+world.boxes==before);
path.nav.loaded=true;path.allow=false;
assert(!survey.Select(&group,&task,from,threat,&space,5000,selected));path.allow=true;
space.allow=false;assert(!survey.Select(&group,&task,from,threat,&space,6000,selected));space.allow=true;
task.OperationRole=Role::SEARCH;path.delta={1.5,0,0};before=path.projections;
assert(survey.Select(&group,&task,from,threat,&space,7000,selected));
assert(path.projections==before+1&&selected.x==101.5); // only the already-selected local footprint
path.delta={2.1,0,0};assert(!survey.Select(&group,&task,from,threat,&space,8000,selected));
path.delta={0,2.1,0};assert(!survey.Select(&group,&task,from,threat,&space,9000,selected));
path.delta={1.5,0,0};world.water=true;
assert(!survey.Select(&group,&task,from,threat,&space,10000,selected)); // water rechecked after projection
path.delta={0,0,0};world.water=false;
world.cliff=true;assert(!survey.Standing(anchor));world.cliff=false;
task.OperationRole=Role::OBSERVE;world.scene=3;
assert(!survey.Select(&group,&task,from,threat,&space,11000,selected)); // corrupt trace cannot invent cover
world.scene=0;
task.OperationRole=Role::FLANK;task.Destination={120,.1,100};task.SupportUntil=14000;
assert(!survey.Select(&group,&task,anchor,threat,&space,12000,selected));
task.SupportUntil=21000;
assert(survey.Select(&group,&task,anchor,threat,&space,13000,selected));
assert(vector::DistanceXZ(anchor,selected)<18); // picks an inward point fitting the remaining lease
assert(task.SupportUntil==21000&&task.Destination.x==120); // no lease extension or task mutation inside survey
// Characters, cars, weapons parented to them and moving props cannot create
// a terrain advantage in a survey. Only static map geometry is admitted.
survey.m_FilterChecks=0;survey.m_FilterExhausted=false;
IEntity wall,character,car,weapon,prop;Physics moving;moving.dynamic=true;
character.character=true;car.vehicle=true;weapon.parent=&character;prop.physics=&moving;
assert(survey.StaticObstacle(&wall));assert(!survey.StaticObstacle(&character));
assert(!survey.StaticObstacle(&car)&&!survey.StaticObstacle(&weapon)&&!survey.StaticObstacle(&prop));
wall.parent=&wall;assert(!survey.StaticObstacle(&wall));wall.parent=nullptr;
survey.m_FilterChecks=512;assert(survey.StaticObstacle(&wall)&&survey.m_FilterExhausted);
// The adapter uses radio-delivered positions, including a newer execution
// report. An older roster position would incorrectly reject this short bound.
Adapter hq;ERR_HQUnit unit;unit.Group=&group;ERR_HQWatchEntry own;ERR_HQBelief belief;
hq.m_Watch.entry=&own;hq.m_Memory.belief=&belief;ERR_HQTask previous;unit.Task=&previous;
ERR_HQOperationSlot slot;ERR_HQExecutionReport report;slot.Report=&report;hq.m_Operations.slot=&slot;
task.SupportUntil=26000;assert(hq.CoordinatedGround(&unit,&task,15000));
assert(vector::DistanceXZ(report.Position,task.Destination)<=20);
vector saved=task.Destination;unit.HomeGuard=true;assert(!hq.CoordinatedGround(&unit,&task,16000));unit.HomeGuard=false;
unit.ReservedForSupport=true;assert(!hq.CoordinatedGround(&unit,&task,16000));unit.ReservedForSupport=false;
unit.LogisticsReservation=1;assert(!hq.CoordinatedGround(&unit,&task,16000));unit.LogisticsReservation=0;
belief.BeliefRevision=3;assert(!hq.CoordinatedGround(&unit,&task,16000));belief.BeliefRevision=2;
own.Closed=true;assert(!hq.CoordinatedGround(&unit,&task,16000));own.Closed=false;
own.Fit=0;assert(!hq.CoordinatedGround(&unit,&task,16000));own.Fit=4;
own.ObservedAt=0;assert(!hq.CoordinatedGround(&unit,&task,120001));
assert(vector::DistanceXZ(saved,task.Destination)==0); // rejected surveys never replace an existing destination
}
''')
source = P.read_text()
for forbidden in ('GetOrigin(', 'GetPlayerManager(', 'GetControlledEntity(', 'Gather(',
'CreateEntity(', 'Spawn(', 'SetOrigin(', 'AddWaypoint(', '.Target'):
assert forbidden not in source
assert 'CoordinatedGround(' not in body(A, 'protected void ShadowCoordinated(')
print('PASS: actual terrain survey chooses usable cover/concealment, preserves search and bound envelopes, honours finite support, delivered evidence, ownership, async tiles and shared geometry budget')