Fix progression stalls and add normal class trainer visits
Core tests / core (push) Successful in 29s

This commit is contained in:
Codex
2026-09-24 09:24:57 +12:00
parent d57d793a84
commit 132bf75f9d
24 changed files with 343 additions and 31 deletions
+7 -1
View File
@@ -8,7 +8,7 @@ test('inference preserves intervening observations and cancels after retirement'
const now=Date.now();
const bot={id:'hero',name:'Zaku',status:'online',tier:'hero',decisions:0,
personality:{activities:['explore']},autonomy:{managed:true},level:5,zone:'Old zone'};
let state={revision:1,bots:[bot],events:[],sequence:0,settings:{}};
let state={revision:1,bots:[bot],events:[],sequence:0,settings:{hardcore:{enabled:false}}};
const store={load:async()=>structuredClone(state),save:async(s,revision)=>{
assert.equal(revision,state.revision);state=s;
}};
@@ -29,3 +29,9 @@ test('inference preserves intervening observations and cancels after retirement'
assert.equal((await runLiveAutonomyTick(store,{}, {},{now,forceBotId:'hero'})).ran,false);
assert.equal(queued,1);
});
test('rest requests never enable indefinite stay',()=>{
const {compileLivePlayerbotIntent}=require('../.test-build/autonomy.js');
const source=compileLivePlayerbotIntent('A',{action:'rest',goal:'Recover',reason:'Tired'});
assert.doesNotMatch(source,/\+stay/);assert.match(source,/-stay/);assert.match(source,/GetHealthPct/);assert.match(source,/AzerothAITrainingOverride/);
});
+1 -1
View File
@@ -17,7 +17,7 @@ function make(id,name)
local p={id=id,name=name,dist=20}
function p:GetName()return self.name end;function p:GetGUIDLow()return self.id end;function p:GetAccountId()return 10+self.id end
function p:IsPlayerbot()return not self.human end;function p:IsDead()return self.dead end;function p:IsInCombat()return self.combat end
function p:IsCasting()return false end;function p:IsTaxi()return false end
function p:IsCasting()return false end;function p:HasUnitState(state)assert(state==0x00000100);return false end
function p:GetBotAI()return {HasGameClientMaster=function()return self.controlled end}end
function p:GetTeam()return self.team or 0 end;function p:GetLevel()return 5 end;function p:GetMapId()return 0 end;function p:GetPhaseMask()return 1 end
function p:GetDistance()return self.dist end;function p:GetGroup()return self.group end
+1 -1
View File
@@ -9,7 +9,7 @@ function CreateLuaEvent(fn)tick=fn end;function PrintError(s)error(s)end
p={ai={}};function GetPlayersInWorld()return {p}end
function p:GetGUIDLow()return 1 end;function p:GetAccountId()return 12 end;function p:GetName()return 'A' end
function p:IsPlayerbot()return true end;function p:GetBotAI()return self.ai end;function p.ai:HasGameClientMaster()return p.human end
function p:IsDead()return p.dead end;function p:IsInCombat()return p.combat end;function p:IsCasting()return false end;function p:IsTaxi()return false end
function p:IsDead()return p.dead end;function p:IsInCombat()return p.combat end;function p:IsCasting()return false end;function p:HasUnitState(state)assert(state==0x00000100);return false end
function p:GetGroup()return p.group end;function p:GetFreeTalentPoints()return 1 end
function p:GetItemByPos(_,slot)if slot==15 then return {IsBroken=function()return true end,GetUInt32Value=function(_,i)return i==0x003D and 50 or 0 end}end end
function p.ai:GetInventoryItems()return {{GetEntry=function()return 10 end,GetCount=function()return 3 end}}end
+6
View File
@@ -28,3 +28,9 @@ test('monitor distinguishes mana recovery, named leader and ghost state',()=>{
b.world.survivalAction='regroup requested';b.world.leaderName='Zaku';assert.equal(activityReason(b,now).label,'Catching up to Zaku');
b.world.isGhost=true;assert.equal(activityReason(b,now).label,'Recovering as a ghost');
});
test('trainer activity reports pending work and verified learning separately',()=>{
const b=bot();Object.assign(b.world,{trainingAt:now,trainingLevel:8,trainingPending:3,trainingLearned:2,trainingTrainer:'Teacher',trainingStatus:'traveling'});
const a=activityReason(b,now);assert.equal(a.label,'Visiting class trainer');assert.match(a.detail,/3 identified spells pending, 2 verified trainer purchases/);
b.world.inCombat=true;assert.equal(activityReason(b,now).label,'In combat');
});
+2 -1
View File
@@ -18,7 +18,7 @@ function player:IsDead()return self.dead end
function player:IsInCombat()return self.combat end
function player:IsCasting()return self.casting end
function player:StopSpellCast()self.casting=false;self.interrupted=true end
function player:IsTaxi()return self.taxi end
function player:HasUnitState(state)assert(state==0x00000100);return self.taxi end
function player:IsRooted()return self.root end
function player:GetGroup()return self.group end
function player:GetX()return self.x end
@@ -58,6 +58,7 @@ l=new();l.execute('step(0);now=now+601;tick();assert(attempts==0)')
print('Intent, movement, health progress, active identity and stale input guards passed')
l=new();l.execute('player.group={GetMembers=function()return {{IsPlayerbot=function()return false end}} end,GetMembersCount=function()return 1 end};step(0);step(601);assert(attempts==0)')
l=new();l.execute('player.motion=7;step(0);step(601);assert(attempts==1 and expired==1)')
l=new();l.execute('function player:IsTaxi()return false end;player.taxi=true;player.motion=7;step(0);step(601);assert(attempts==0 and expired==0,"In-flight state protects a player even though IsTaxi NPC predicate is false")')
print('Human group exclusion and stale flight recovery passed')
l=new();l.execute("step(0);step(600);player.x=8;step(30);assert(string.find(captured[#captured], '726573756D6564'))")
+5 -1
View File
@@ -15,7 +15,7 @@ function make(id,name)
function p:GetGUIDLow()return self.id end;function p:GetName()return self.name end;function p:GetAccountId()return 10+self.id end
function p:IsPlayerbot()return not self.human end;function p:IsDead()return self.dead end;function p:GetBotAI()return self.ai end
function p:GetPowerType()return self.powerType or 1 end;function p:GetPowerPct()return self.mana or 100 end;function p:GetAttackers()return self.attackers or {} end;function p:GetHealthPct()return self.hp end;function p:IsInCombat()return self.combat end
function p:IsTaxi()return self.taxi end;function p:IsCasting()return self.cast end;function p:IsRooted()return false end
function p:HasUnitState(state)assert(state==0x00000100);return self.taxi end;function p:IsCasting()return self.cast end;function p:IsRooted()return false end
function p:GetX()return self.x or 0 end;function p:GetY()return 0 end
function p:GetUnfriendlyUnitsInRange()return self.friendly and {} or self.selected and {self.selected} or {} end;function p:GetSelection()return self.selected end;function p:StopSpellCast()self.cast=false;self.interrupted=true end
function p:AttackStop()self.attackStopped=true end;function p:MoveStop()self.waited=true end
@@ -65,3 +65,7 @@ l=new();l.execute("group();A.distance=50;B.distance=50;A.strategies={rpg=true,gr
l=new();l.execute("group();B.distance=50;B.combat=true;B.hp=20;step(0);assert(calls[1]=='flee','Survival outranks regrouping');assert(not B.master)")
l=new();l.execute("group();A.distance=40;step(0);A.waited=false;step(22);assert(not A.waited,'Leader waiting must remain bounded');A.distance=5;step(2);A.distance=40;step(2);assert(A.waited,'A fresh separation starts a fresh wait')")
print('Cohesion suppresses competing work, keeps catch-up hysteresis, yields to danger, and resets bounded waits')
l=new();l.execute("A.strategies={stay=true};step(0);assert(not A.strategies.stay,'Fully recovered autonomous resident must not remain in stay')")
l=new();l.execute("group();B.distance=50;step(0);step(40);assert(B.master==nil,'No-progress regroup must release its owned master');assert(not AzerothAISurvivalOverride(B));step(30);assert(B.master==nil,'Backoff must prevent immediate retry')")
print('Recovered stay cleared and non-progressing regroup bounded')
+36
View File
@@ -0,0 +1,36 @@
from pathlib import Path
from lupa import LuaRuntime
source=(Path(__file__).resolve().parents[1]/'ale/azerothai_training.lua').read_text()
setup='''
now=1000;os.time=function()return now end;calls={};moves=0;learned=0;team=2;callbacks={}
function AuthDBExecute()end
function AuthDBQuery(s)
if s:find("state='active'",1,true) then return {GetUInt32=function(_,i)return i==0 and 1 or 11 end,GetString=function()return 'A' end,NextRow=function()return false end} end
if s:find('SELECT flags',1,true) then return {GetString=function()return 'new rpg,quest' end}end
end
function WorldDBQuery()return {GetUInt32=function(_,i)return ({[0]=8,[1]=100,[2]=0,[7]=team})[i]end,GetFloat=function(_,i)return ({[3]=10,[4]=20,[5]=30})[i]end,GetString=function()return 'Trainer' end,NextRow=function()return false end}end
function CreateLuaEvent(f)tick=f end;function PrintError(s)error(s)end
p={ai={},money=100,level=4,distance=0,pending=2}
function p:GetGUIDLow()return 1 end;function p:GetAccountId()return 11 end;function p:GetName()return 'A' end
function p:GetLevel()return self.level end;function p:GetClass()return 8 end;function p:GetTeam()return 0 end;function p:GetMapId()return 0 end
function p:GetX()return 10 end;function p:GetY()return 20 end;function p:IsPlayerbot()return true end;function p:GetBotAI()return self.ai end
function p.ai:HasGameClientMaster()return p.human end;function p:IsDead()return p.dead end;function p:IsInCombat()return p.combat end
function p:IsCasting()return false end;function p:HasUnitState(state)assert(state==0x00000100);return self.taxi end;function p:GetGroup()return p.group end;function p:GetCoinage()return self.money end
function p.ai:GetClassTrainerStatus()return p.pending,10,20 end
function p.ai:TrainAtClassTrainer()assert(p.distance<=5);local n=p.pending;learned=learned+n;p.pending=0;p.money=p.money-20;return n end
function p.ai:HasStrategy()return true end;function p.ai:ChangeStrategy(s)table.insert(calls,s)end
function p:GetNearestCreature()return {} end;function p:GetDistance()return self.distance end;function p:MoveStop()end;function p:MoveTo()moves=moves+1 end
function GetPlayersInWorld()return {p}end
function AzerothAISurvivalOverride()return p.recovering end
'''
def new(extra=''):
l=LuaRuntime();l.execute(setup+extra);l.execute(source);return l
l=new();l.execute("tick();assert(learned==2 and p.money==80);assert(AzerothAITrainingSnapshot(p).trainingStatus=='no new trainable spells');now=1010;p.level=5;p.pending=1;tick();assert(learned==3,'New level is checked without waiting a minute')")
for flag in ['human','dead','combat','recovering','taxi']:
l=new();l.execute(f'p.{flag}=true;tick();assert(learned==0 and moves==0)')
l=new();l.execute("p.money=0;tick();assert(learned==0 and moves==0 and AzerothAITrainingSnapshot(p).trainingStatus=='saving for training')")
l=new();l.execute("p.distance=20;tick();assert(learned==0 and moves==1 and AzerothAITrainingOverride(p));now=1060;tick();assert(not AzerothAITrainingOverride(p),'Stalled visit releases held work')")
l=new('team=4');l.execute("tick();assert(learned==0 and moves==0,'Opposing faction trainer excluded')")
l=new();l.execute("p.distance=20;tick();assert(AzerothAITrainingOverride(p));p.human=true;now=1010;tick();assert(not AzerothAITrainingOverride(p) and moves==1,'Human takeover releases our lease without continuing travel')")
l=new();l.execute("p.group={IsRaidGroup=function()return false end,GetMembersCount=function()return 2 end,GetMembers=function()return {p}end};tick();assert(learned==0 and moves==0)")
print('Trainer checks: every level, verified learning, money, interaction distance, stalled-path release and human/combat/death/group/faction guards passed')