from pathlib import Path from lupa import LuaRuntime setup=''' now=1000;os.time=function()return now end;calls={};people={} function AuthDBQuery(sql)if string.find(sql,'survival_holds',1,true) then return nil end;local i=1;return {GetUInt32=function(_,n)return n==0 and i or 10+i end,GetString=function()return i==1 and 'A' or 'B'end,NextRow=function()i=i+1;return i<=2 end} end function GetPlayersInWorld()return people end function GetPlayerByName(n)for _,p in pairs(people)do if p.name==n then return p end end end function CreateLuaEvent(fn,ms)if ms==2000 then tick=fn end end function RegisterPlayerEvent()end auditRows={};function AuthDBExecute(sql)table.insert(auditRows,sql)end function PrintInfo()end function PrintError(s)error(s)end function make(id,name) local p={id=id,name=name,hp=100,distance=25,ai={}} 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: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 function p:GetGroup()return self.group end;function p:GetVictim()return self.enemy end;function p:GetLevel()return 5 end function p:GetDistance()return self.distance end;function p:GetMapId()return 0 end;function p:GetPhaseMask()return 1 end function p.ai:GetMaster()return p.master end;function p.ai:SetMaster(m)p.master=m end function p.ai:HasGameClientMaster()return p.controlled end;function p.ai:IsMovementImpaired()return false end function p.ai:HasStrategy(s)return p.strategies and p.strategies[s] or false end function p.ai:GetGroupLeader()return leader end;function p.ai:ChangeStrategy(s,state)p.strategies=p.strategies or {};p.strategies[string.sub(s,2)]=string.sub(s,1,1)=='+' end function p.ai:DoSpecificAction(s)table.insert(calls,s);return true end return p end A=make(1,'A');B=make(2,'B');people={A,B};leader=A function group() local g={GetMembers=function()return {A,B}end,GetMembersCount=function()return 2 end,IsRaidGroup=function()return false end};A.group=g;B.group=g end function step(n)now=now+n;tick()end ''' def new(): l=LuaRuntime();l.execute(setup);l.execute((Path(__file__).resolve().parents[1]/'ale/azerothai_survival.lua').read_text());l.execute('AzerothAISetSurvivalEnabled(true)');return l l=new();l.execute("A.combat=true;A.hp=20;step(0);assert(calls[1]=='flee');step(2);assert(#calls==1);step(6);assert(#calls==2)") l=new();l.execute("group();A.human=true;B.combat=true;B.hp=20;step(0);assert(#calls==0)") l=new();l.execute("group();step(0);assert(calls[1]=='follow' and B.master==A)") l=new();l.execute("group();B.distance=250;step(0);assert(#calls==0)") l=new();l.execute("A.combat=true;A.hp=20;A.controlled=true;step(0);assert(#calls==0)") l=new();l.execute("A.combat=true;A.hp=20;step(0);A.combat=false;step(20);assert(calls[2]=='eat' and calls[3]=='drink')") l=new();l.execute("group();AzerothAISetSurvivalEnabled(false);step(0);assert(#calls==0)") print('Native survival: retreat cooldown, recovery, regroup range, human-party protection, and disabled grouping passed') l=new();l.execute("A.combat=true;A.hp=45;A.cast=true;step(0);assert(A.interrupted and calls[1]=='flee')") l=new();l.execute("A.combat=true;A.hp=100;A.enemy={GetGUID=function()return 999 end,GetLevel=function()return 8 end,ToCreature=function()return nil end,IsHostileTo=function()return true end};step(0);assert(calls[1]=='flee')") l=new();l.execute("A.selected={GetGUID=function()return 99 end,GetLevel=function()return 5 end,ToCreature=function(self)return self end,GetRank=function()return 1 end,IsHostileTo=function()return true end};step(0);assert(A.attackStopped and calls[1]=='flee')") l=new();l.execute("A.friendly=true;A.selected={GetGUID=function()return 999 end,GetLevel=function()return 80 end,ToCreature=function()return nil end};step(0);assert(#calls==0)") l=new();l.execute("A.combat=true;A.hp=40;step(0);A.x=10;step(2);A.combat=false;step(2);local found=false;for _,q in pairs(auditRows)do if string.find(q,'6F75742D6F662D636F6D626174') then found=true end end;assert(found)") l=new();l.execute("A.combat=true;A.hp=40;step(0);A.hp=70;step(30);assert(string.find(auditRows[#auditRows], '6E6F2D6D6F76656D656E74'))") l=new();l.execute("group();B.distance=150;step(0);assert(B.master==A)") l=new();l.execute("group();A.distance=40;step(0);assert(A.waited);A.waited=false;step(22);assert(not A.waited)") print('Earlier retreat, casting interruption, hostile/elite avoidance, bounded leader waiting, longer regroup and observed retreat outcomes passed') l=new();l.execute("A.hp=80;A.strategies={rpg=true,grind=true,gather=true};step(0);assert(calls[1]=='eat' and A.strategies.rpg==false and A.strategies.grind==false);assert(AzerothAISurvivalOverride(A));A.hp=90;step(2);assert(AzerothAISurvivalOverride(A))") l=new();l.execute("A.powerType=0;A.mana=20;step(0);assert(calls[1]=='eat' and calls[2]=='drink' and AzerothAISurvivalOverride(A))") l=new();l.execute("A.combat=true;A.hp=75;A.attackers={1,2};step(0);assert(calls[1]=='flee')") l=new();l.execute("A.combat=true;step(0);A.hp=75;step(2);assert(calls[1]=='flee')") l=new();l.execute("A.hp=80;step(0);A.hp=100;local query=AuthDBQuery;function AuthDBQuery(sql)if string.find(sql,'survival_holds WHERE guid=1',1,true) then return {GetUInt32=function(_,i)return ({[0]=1,[2]=11,[3]=1,[4]=0,[5]=0,[6]=0,[7]=0})[i] end,GetString=function()return 'A' end} end;return query(sql)end;step(30);assert(A.strategies.rpg==true);assert(not AzerothAISurvivalOverride(A))") print('Readiness holds, mana recovery, outnumbered/rapid-loss retreat, and original-strategy restoration passed') l=new();l.execute("A.combat=true;A.hp=75;A.attackers={1,2};A.enemy={GetGUID=function()return 999 end,GetLevel=function()return 5 end,ToCreature=function()return nil end};step(0);A.selected=A.enemy;A.combat=false;A.hp=100;A.attackers={};step(20);assert(A.attackStopped and calls[#calls]=='flee');A.attackStopped=false;step(601);assert(not A.attackStopped)") print('Dangerous encounter target avoidance expires after ten minutes') l=new();l.execute("group();A.distance=50;B.distance=50;A.strategies={rpg=true,grind=true};B.strategies={rpg=true,gather=true};step(0);assert(A.waited and not A.strategies.rpg and not B.strategies.gather);assert(AzerothAISurvivalOverride(A) and AzerothAISurvivalOverride(B));B.distance=9;step(8);assert(AzerothAISurvivalOverride(B));B.distance=5;A.distance=5;step(30);assert(not AzerothAISurvivalOverride(B),'Caught-up bot must release the hold')") 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')