Dota 2 - Simple LUA AI

I have encountered many questions about AI on the modding irc over the time, so I decided to write up a tutorial for a very basic AI that can be used in Lua. The term AI might seem intimidating as a programmer that has little to no experience with it, I will try however to lay out the process for a simple state-driven AI in a way that is as clear as possible. Hopefully by the end of this article writing your own AI does not seem as scary anymore.

What are we making

For the sake of this tutorial I will stick to a very basic state-driven AI, which as you will see is already pretty useful for a lot of AI behaviour you encounter in dota 2. To make this example a little more specific we will try to make a simple version of the AI that neutrals use in dota 2 at the moment. This entails that they stay at their spawn position until an enemy unit comes into range. When an enemy unit has been spotted the AI will start attacking the unit and chase it. Once the enemy unit has been killed, or the AI has run out of range of its spawnpoint, the AI will run back to the spawnpoint, ignoring any enemies nearby. Once it is back at the spawn point, the AI will go back to watching for enemy units and the cycle repeats.

Planning

The first phase to making reliable AI (in the sense that it will always do what you expect it to) is planning. I personally think that making a formal diagram representing the different states and the transitions between these states are a big help when making an AI like this. The more effort you put into this diagram, the easier the actual implementation of your AI will be.

For our neutral example I have translated the text describing the unit's behaviour into a state diagram, which contains all possible states and the conditions for transitioning between these states. The result is the following diagram:
A simple AI state diagram
These diagrams can be made with any software with drawing possibilities such as paint, photoshop or word. I really like using https://www.draw.io/, which is an online drawing tool specialised for drawing diagrams and graphs.

In the diagram you can see the different states represented by boxes and transitions plus their conditions by arrows.

Implementation

Now that we have a good idea of what we're going to make, this AI looks a lot easier already and we can start implementing. For this example I will use AI coded from scratch just to let you see how everything works. Remember that there are many ways to actually implement this and this is just one way to do it.

The skeleton

As a basis for this AI we will use a simple skeleton that basically just makes a new class, which will implement our logic. The skeleton code looks like this:
--[[
	SimpleAI
	A simple Lua AI to demonstrate implementation of a simple AI for dota 2.

	By: Perry
	Date: August, 2015
]]

--AI parameter constants
AI_THINK_INTERVAL = 0.5 -- The interval in seconds between two think ticks

--AI state constants
AI_STATE_NULL = 0 --This will be replaced when we add our states

--Define the SimpleAI class
SimpleAI = {}
SimpleAI.__index = SimpleAI

--[[ Create an instance of the SimpleAI class for some unit 
  with some parameters. ]]
function SimpleAI:MakeInstance( unit, params )
	--Construct an instance of the SimpleAI class
	local ai = {}
	setmetatable( ai, SimpleAI )

	--Set the core fields for this AI
	ai.unit = unit --The unit this AI is controlling
	ai.state = AI_STATE_NULL --The initial state
	ai.stateThinks = { --Add thinking functions for each state
		[AI_STATE_NULL] = function() end --Empty function for now
	}

	--Start thinking
	Timers:CreateTimer( ai.GlobalThink, ai )

	--Return the constructed instance
	return ai
end

--[[ The high-level thinker this AI will do every tick, selects the correct
  state-specific think function and executes it. ]]
function SimpleAI:GlobalThink()
	--If the unit is dead, stop thinking
	if not self.unit:IsAlive() then
		return nil
	end

	--Execute the think function that belongs to the current state
	Dynamic_Wrap(SimpleAI, self.stateThinks[ self.state ])( self )

	--Reschedule this thinker to be called again after a short duration
	return AI_THINK_INTERVAL
end
I feel like a short explanation is in order. What this skeleton does is basically just create the SimpleAI class, which will think at a certain interval (1 second in this example), and will execute some think function depending on which state it is in. Right now there is an empty function defined for the null state, but we will be replacing this with actual states like we defined next. A unit can now be made an instance of this AI by calling:
SimpleAI:MakeInstance( unit, {...} )

Adding states

So now we have our skeleton it is time to look back at the diagram we made before. First lets add some empty think functions for each state. First we replace the state constants at the top of the code with meaningful constants like so:
AI_STATE_IDLE = 0
AI_STATE_AGGRESSIVE = 1
AI_STATE_RETURNING = 2
Next up we add some empty thinking functions for each state at the bottom of our file like so:
--[[ Think function for the 'Idle' state. ]]
function SimpleAI:IdleThink()

end

--[[ Think function for the 'Aggressive' state. ]]
function SimpleAI:AggressiveThink()

end

--[[ Think function for the 'Returning' state. ]]
function SimpleAI:ReturningThink()

end
And finally we modify the SimpleAI:MakeInstance function to include these states. Note that I have also added some parameters here which we will need for the state logic.
--[[ Create an instance of the SimpleAI class for some unit 
  with some parameters. ]]
function SimpleAI:MakeInstance( unit, params )
	--Construct an instance of the SimpleAI class
	local ai = {}
	setmetatable( ai, SimpleAI )

	--Set the core fields for this AI
	ai.unit = unit --The unit this AI is controlling
	ai.state = AI_STATE_IDLE --The initial state
	ai.stateThinks = { --Add thinking functions for each state
		[AI_STATE_IDLE] = 'IdleThink',
		[AI_STATE_AGGRESSIVE] = 'AggressiveThink',
		[AI_STATE_RETURNING] = 'ReturningThink'
	}

	--Set parameters values as fields for later use
	ai.spawnPos = params.spawnPos
	ai.aggroRange = params.aggroRange
	ai.leashRange = params.leashRange

	--Start thinking
	Timers:CreateTimer( ai.GlobalThink, ai )

	--Return the constructed instance
	return ai
end

State logic

Now all the skeleton code for our specific AI is in place we can think about what logic to implement per state. Each state evaluation has two main parts. The first part is checking if the AI should move to any other state, the second part is the behaviour the unit has to repeat while in that state. Now the behaviour inside the state you should be able to figure out yourself, the transitions are the fun AI part. I will demonstrate how the diagram we made translates into code for the most complex state we have in there, namely the 'Aggressive' state. We are already in the 'Aggressive' state, so the first thing we have to do in this state is checking if we have to move to the next state. Once all checks are done we can implement the behaviour to be executed when staying inside this state.
--[[ Think function for the 'Aggressive' state. ]]
function SimpleAI:AggressiveThink()
	--Check if the unit has walked outside its leash range
	if ( self.spawnPos - self.unit:GetAbsOrigin() ):Length() > self.leashRange then
		self.unit:MoveToPosition( self.spawnPos ) --Move back to the spawnpoint
		self.state = AI_STATE_RETURNING --Transition the state to the 'Returning' state(!)
		return true --Return to make sure no other code is executed in this state
	end

	--Check if the unit's target is still alive (self.aggroTarget will have to be set when transitioning into this state)
	if not self.aggroTarget:IsAlive() then
		self.unit:MoveToPosition( self.spawnPos ) --Move back to the spawnpoint
		self.state = AI_STATE_RETURNING --Transition the state to the 'Returning' state(!)
		return true --Return to make sure no other code is executed in this state
	end

	--State behavior
	--Here we can just do any behaviour you want to repeat in this state
	print('Attacking!')
end

Now we just can do a little check to see if we did it right:


As you can see this translation is actually really easy, and corresponds directly to the diagram we made!

Now just add the other states like this:
--[[ Think function for the 'Idle' state. ]]
function SimpleAI:IdleThink()
	--Find any enemy units around the AI unit inside the aggroRange
	local units = FindUnitsInRadius( self.unit:GetTeam(), self.unit:GetAbsOrigin(), nil,
		self.aggroRange, DOTA_UNIT_TARGET_TEAM_ENEMY, DOTA_UNIT_TARGET_ALL, DOTA_UNIT_TARGET_FLAG_NONE, 
		FIND_ANY_ORDER, false )

	--If one or more units were found, start attacking the first one
	if #units > 0 then
		self.unit:MoveToTargetToAttack( units[1] ) --Start attacking
		self.aggroTarget = units[1]
		self.state = AI_STATE_AGGRESSIVE --State transition
		return true
	end

	--State behavior
	--Whistle a tune
end

--[[ Think function for the 'Returning' state. ]]
function SimpleAI:ReturningThink()
	--Check if the AI unit has reached its spawn location yet
	if ( self.spawnPos - self.unit:GetAbsOrigin() ):Length() < 10 then
		--Go into the idle state
		self.state = AI_STATE_IDLE
		return true
	end
end

Once again you can check the functions to see if they correspond to the diagram we made. If you're satisfied with this you are done, your first AI officially works now! All you have to do to initialise it is call:
SimpleAI:MakeInstance( unit, { spawnPos = unit:GetAbsOrigin(), aggroRange = 500, leashRange = 900 } )

Note on neutral npc_dota_creature

When using units that have baseclass npc_dota_creature assigned to the DOTA_TEAM_NEUTRALS team, they will default to executing neutral behaviour, executing valve's normal neutral AI. This overrides any AI you attack to them using this method. This can be turned off by adding "UseNeutralCreepBehavior" "0" to the unit's definition in KV.

Final code

--[[
	SimpleAI
	A simple Lua AI to demonstrate implementation of a simple AI for dota 2.

	By: Perry
	Date: August, 2015
]]

--AI parameter constants
AI_THINK_INTERVAL = 0.5 -- The interval in seconds between two think ticks

--AI state constants
AI_STATE_IDLE = 0
AI_STATE_AGGRESSIVE = 1
AI_STATE_RETURNING = 2

--Define the SimpleAI class
SimpleAI = {}
SimpleAI.__index = SimpleAI

--[[ Create an instance of the SimpleAI class for some unit 
  with some parameters. ]]
function SimpleAI:MakeInstance( unit, params )
	--Construct an instance of the SimpleAI class
	local ai = {}
	setmetatable( ai, SimpleAI )

	--Set the core fields for this AI
	ai.unit = unit --The unit this AI is controlling
	ai.state = AI_STATE_IDLE --The initial state
	ai.stateThinks = { --Add thinking functions for each state
		[AI_STATE_IDLE] = 'IdleThink',
		[AI_STATE_AGGRESSIVE] = 'AggressiveThink',
		[AI_STATE_RETURNING] = 'ReturningThink'
	}

	--Set parameters values as fields for later use
	ai.spawnPos = params.spawnPos
	ai.aggroRange = params.aggroRange
	ai.leashRange = params.leashRange

	--Start thinking
	Timers:CreateTimer( ai.GlobalThink, ai )

	--Return the constructed instance
	return ai
end

--[[ The high-level thinker this AI will do every tick, selects the correct
  state-specific think function and executes it. ]]
function SimpleAI:GlobalThink()
	--If the unit is dead, stop thinking
	if not self.unit:IsAlive() then
		return nil
	end

	--Execute the think function that belongs to the current state
	Dynamic_Wrap(SimpleAI, self.stateThinks[ self.state ])( self )

	--Reschedule this thinker to be called again after a short duration
	return AI_THINK_INTERVAL
end

--[[ Think function for the 'Idle' state. ]]
function SimpleAI:IdleThink()
	--Find any enemy units around the AI unit inside the aggroRange
	local units = FindUnitsInRadius( self.unit:GetTeam(), self.unit:GetAbsOrigin(), nil,
		self.aggroRange, DOTA_UNIT_TARGET_TEAM_ENEMY, DOTA_UNIT_TARGET_ALL, DOTA_UNIT_TARGET_FLAG_NONE, 
		FIND_ANY_ORDER, false )

	--If one or more units were found, start attacking the first one
	if #units > 0 then
		self.unit:MoveToTargetToAttack( units[1] ) --Start attacking
		self.aggroTarget = units[1]
		self.state = AI_STATE_AGGRESSIVE --State transition
		return true
	end

	--State behavior
	--Whistle a tune
end

--[[ Think function for the 'Aggressive' state. ]]
function SimpleAI:AggressiveThink()
	--Check if the unit has walked outside its leash range
	if ( self.spawnPos - self.unit:GetAbsOrigin() ):Length() > self.leashRange then
		self.unit:MoveToPosition( self.spawnPos ) --Move back to the spawnpoint
		self.state = AI_STATE_RETURNING --Transition the state to the 'Returning' state(!)
		return true --Return to make sure no other code is executed in this state
	end

	--Check if the unit's target is still alive (self.aggroTarget will have to be set when transitioning into this state)
	if not self.aggroTarget:IsAlive() then
		self.unit:MoveToPosition( self.spawnPos ) --Move back to the spawnpoint
		self.state = AI_STATE_RETURNING --Transition the state to the 'Returning' state(!)
		return true --Return to make sure no other code is executed in this state
	end

	--State behavior
	--Here we can just do any behaviour you want to repeat in this state
	print('Attacking!')
end

--[[ Think function for the 'Returning' state. ]]
function SimpleAI:ReturningThink()
	--Check if the AI unit has reached its spawn location yet
	if ( self.spawnPos - self.unit:GetAbsOrigin() ):Length() < 10 then
		--Go into the idle state
		self.state = AI_STATE_IDLE
		return true
	end
end