Capture current mission, AI, map and administration development
Include the current addon, terrain integrations, tools, regression tests and recorded development evidence.
This commit is contained in:
@@ -0,0 +1,224 @@
|
||||
"""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')
|
||||
Reference in New Issue
Block a user