Lua AI Legacy Methods Howto
Warning |
This page describes some of the old methods available for setting up Lua AI functionality. They are still working and described here because quite a bit of existing code still uses them. However, a new method is now available for this and should be used instead for new AI development.
Also note that this page is not being updated any more. |
Contents
Lua AI Setup
This page is meant as a practical guide for setting up and testing Lua AI functionality in a scenario. It does not give a complete description of Lua or Lua AI. See LuaAI or LuaWML for that. Also note that we do not explain all the different ways of setting things up here. Instead, we focus on one specific method that works reasonably well. Using variations thereof will hopefully be straightforward once the principles are understood.
Setting up a Lua AI requires the definition of a Lua AI engine in the [side] tag and one or more stages executing the individual AI actions. We restrict ourselves here to the example of using the default main_loop stage with its candidate actions.
The example listed below is that of the Simple Lua AI Demo scenario of the AI-demos add-on. You can go there if you want to see this used in an actual campaign.
Unlike Formula AI and the default cpp AI, Lua AI requires an engine to be defined (this is obsolete). Specifically, it does not work in [modify_side]. Here is an example of such an [ai] tag:
[ai]
version=10710
[engine]
name="lua"
code= <<
local ai = ...
return wesnoth.require("~add-ons/AI-demos/lua/luaai-demo_engine.lua").init(ai)
>>
[/engine]
[stage]
id=main_loop
name=ai_default_rca::candidate_action_evaluation_loop
{AI_CA_GOTO}
{AI_CA_RECRUITMENT}
{AI_CA_MOVE_LEADER_TO_GOALS}
{AI_CA_MOVE_LEADER_TO_KEEP}
{AI_CA_COMBAT}
{AI_CA_HEALING}
{AI_CA_VILLAGES}
{AI_CA_RETREAT}
{AI_CA_MOVE_TO_TARGETS}
{AI_CA_LEADER_SHARES_KEEP}
[candidate_action]
engine=lua
name=move_spearmen
id=move_spearmen
evaluation="return (...):move_unittype_eval('Spearman', 100010)"
execution="(...):move_unittype_exec('Spearman', 4, 20)"
[/candidate_action]
[candidate_action]
engine=lua
name=move_bowmen
id=move_bowmen
evaluation="return (...):move_unittype_eval('Bowman', 99990)"
execution="(...):move_unittype_exec('Bowman', 34, 20)"
[/candidate_action]
[/stage]
[/ai]
Note that a lot of this is simply setting up the default main_loop stage and candidate actions. The important non-default lines are:
The 'version=' line is required and ensures that we are using a version of the AI that can use a Lua AI engine.
In the [engine] tag, the 'name=' key tells Wesnoth that we are going to use the Lua engine. 'ai = ...' means that all the default Lua AI functions will be available through the ai table (they get handed down from the process creating the engine through Lua's variadic function '...').
The engine returns the return value of function init in file ~add-ons/AI-demos/lua/luaai-demo_engine.lua. This sets up the custom AI functions. Note the we are passing the ai table defined in the previous line to init(), in order to make it available to the custom AI functions.
The remaining custom lines are setting up the custom candidate actions at the end of the code, which call the move_unittype_eval() and move_unittype_exec() functions. These are defined in the luaai-demo_engine.lua file that is included in the [engine] tag:
return {
init = function(ai)
local luaai_demo = {}
local H = wesnoth.require "lua/helper.lua"
function luaai_demo:move_unittype_eval(type, score)
local units = wesnoth.get_units {
side = wesnoth.current.side,
type = type,
formula = '$this_unit.moves > 0'
}
if units[1] then return score end
return 0
end
function luaai_demo:move_unittype_exec(type, goal_x, goal_y)
local unit = wesnoth.get_units {
side = wesnoth.current.side,
type = type,
formula = '$this_unit.moves > 0'
}[1]
-- Find path toward the goal
local path, cost = wesnoth.find_path(unit, goal_x, goal_y)
-- If there's no path to goal, or all path hexes are occupied,
-- use current position as default
local next_hop = { unit.x, unit.y }
-- Go through path to find farthest reachable, unoccupied hex
-- Start at second index, as the first is just the unit position itself
for i = 2,#path do
local sub_path, sub_cost = wesnoth.find_path( unit, path[i][1], path[i][2])
-- If we can get to that hex, check whether it is occupied
-- (can only be own or allied units as enemy unit hexes are not reachable)
if sub_cost <= unit.moves then
local unit_in_way = wesnoth.get_unit(path[i][1], path[i][2])
if not unit_in_way then
next_hop = path[i]
end
else -- otherwise stop here; rest of path is outside movement range
break
end
end
--print('Moving:', unit.id, '-->', next_hop[1], next_hop[2])
ai.move_full(unit, next_hop[1], next_hop[2])
end
return luaai_demo
end
}
These functions set up and return a Lua table called luaai_demo which contains two functions move_unittype_eval() and move_unittype_exec(). These are the evaluation and execution functions that are called by the custom AI candidate actions.
Note that the AI behavior of this example is very simple and meant as a demonstration only. It sends all units of a certain type to a certain location on the map. Note that the unit type, the location as well as the candidate action evaluation score are parameters that are passed to the functions in the [candidate_action] tags. Thus, it is possible to send Spearmen and Bowmen to different locations. Furthermore, by using evaluation scores just below and above the default combat CA, we can have the bowmen attack enemy units within reach while the spearmen avoid all combat. See what effect this has in practice by checking out the Simple Lua AI Demo scenario in the AI-demos add-on.
Notes:
- Important: The evaluation functions may not change the game state. Doing that will lead to OOS errors and may even crash the game.
- With Formula AI it is possible to add candidate actions without specifically defining the RCA AI stage. That is not possible in Lua AI, the engine and main_loop stage always need to be set up.
The data Variable
In the example above, the variable units (or unit) is defined separately in both the evaluation and the execution functions. This is not a big deal here as it is a simple (and quick) function call. Imagine however that we were doing a long evaluation of, say, the best possible attack out of a myriad of different options, that we then want to evaluate against a retreat option evaluated in a different candidate action. Both the combat and the retreat CA return their score, and the CA with the higher score is executed. If combat is chosen, we want to avoid having to redo the entire attack evaluation in the execution function just to determine, again, which attack is the best.
This duplication of code and additional calculation time can be avoided using the built-in persistent data variable. This variable is automatically produced by the engine as a field of the table returned by the custom AI. It has the added advantage that it persists through save/load cycles (see LuaAI#Persistence). It can be accessed through variable self.data inside the custom AI code. In the example above we could use:
self.data.units = wesnoth.get_units { insert filter here }
in the evaluation function and
local unit = self.data.units[1]
in the execution function.
Note: since move_unittype_eval() and move_unittype_exec() are written as methods of object luaai_demo (through use of ':'), self refers to the object holding them: luaai_demo. One could just as well use luaai_demo.data.units here.
Testing setup
*** This section has not yet been updated***
If we were using only the code described above, Wesnoth would have to be restarted (or the cache cleared with F5) every time we made a change to the code. That is very tedious and slow for development and testing purposes. There are much quicker options to accomplish this. One of them is to include the code in a file and then type:
:lua wesnoth.dofile(filename)
in the Wesnoth window. However, there is an even easier method through the right-click menu. In order for this to work, we need:
- The AI to be initialized, but the player having control of the side
- A right-click menu option to run Lua engine code
- Specific attention is needed if the data variable is to be used
Initializing the AI
Let's assume that Side 1 is the side for which we want to develop an AI and Side 2 is the side against which we want to test it. We need to have control of Side 1, so that the correct side's units can be manipulated using the built-in Lua AI functions (otherwise we get 'move_result::E_NOT_OWN_UNIT' errors). However, these functions are only initialized when needed, that is, when the AI plays through its first turn. Furthermore, if the ai variable in the engine is defined as a local variable, they are deleted as soon as the AI turn is done and redefined only for the next turn.
We circumvent these problems by using two little tricks. First, we define that ai variable as global, meaning it needs to be defined first in the preload event, and it needs to be defined as 'ai = ...' rather than 'local ai = ...' in the engine, as described above. Second, we let the AI play through its first turn, and then change the controller to human for the second turn. This can be done either manually in debug mode using ':droid 1', or automatically by including the following event in the scenario:
[event] name=side 1 turn 1 end [modify_side] side=1 controller=human [/modify_side] [/event]
If this event is used, it does not matter whether Side 2 is human or AI controlled, but setting it up as AI controlled means that we are immediately placed at the beginning of Turn 2 in control of Side 1, ready to test our AI code.
As a side note, it is also possible to test the AI code by controlling Side 1 units while being in (human) control of Side 2 (after the ai variable has been initialized as described above). It is, in that case, however more difficult to move those units around for testing purposes.
Right-click Menu Option to Execute Lua Code
In order to avoid having to restart Wesnoth every time we make a change, we can place the Lua code to be tested in a file ('~add-ons/LuaAI_tests/lua_manual.lua' for this example). This file can be executed at the command line by typing (or copy-pasting):
:lua wesnoth.dofile "~add-ons/LuaAI_tests/lua_manual.lua"
Even easier is placing this in a macro providing a right-click menu option:
#define LUA_RELOAD_MENU [event] name=start [set_menu_item] id = m01 description=_"Reload Lua code" image=items/ring-red.png~CROP(26,26,20,20) [command] [lua] code=<<wesnoth.dofile "~add-ons/LuaAI_tests/lua_manual.lua">> [/lua] [/command] [/set_menu_item] [/event] #enddef
Once we place {LUA_RELOAD_MENU} in the [scenario] tag, all we need to do is choose this option from the right-click menu every time we change code in the file and test at will.
Putting It All Together: The Testing Environment
To summarize, we want to have the following pieces in place for easy testing of Lua AI code (using the previous example):
- Macro {LUA_AI_PRELOAD} as defined above, placed inside the [scenario] tag
- An empty Lua engine definition, and the stages and candidate actions, placed inside the [side] tag as follows:
[ai] version=10710 [engine] name="lua" code= << --! ============================================================== --local ai = ... -- use this for the final version ai = ... -- use this for testing local my_ai = {} return my_ai --! ============================================================== >> [/engine] [stage] id=main_loop name=testing_ai_default::candidate_action_evaluation_loop {AI_CA_GOTO} {AI_CA_RECRUITMENT} {AI_CA_MOVE_LEADER_TO_GOALS} {AI_CA_MOVE_LEADER_TO_KEEP} {AI_CA_COMBAT} {AI_CA_HEALING} #{AI_CA_VILLAGES} #{AI_CA_RETREAT} {AI_CA_MOVE_TO_TARGETS} {AI_CA_PASSIVE_LEADER_SHARES_KEEP} [candidate_action] engine=lua name=go_south evaluation="return (...):go_south_evaluation()" execution="(...):go_south_execution()" [/candidate_action] [/stage] [ai]
The stage and candidate action definition is, in principle, optional at this point, however, some stage doing something on Turn 1 is required, otherwise the ai variable is never defined (it only gets defined when it is needed).
- Side 1 to be set up with 'controller=ai', and the 'side 1 turn 1 event' defined above placed inside the [scenario] tag
- Macro {LUA_RELOAD_MENU} as defined above, placed inside the [scenario] tag
- Finally, we want a file '~add-ons/LuaAI_tests/lua_manual.lua' with the following content:
local my_ai = {} function my_ai:go_south_evaluation() local units = wesnoth.get_units { side = wesnoth.current.side, formula = "(hitpoints < max_hitpoints) and ($this_unit.moves > 0)" } local value = 0 if units[1] then value = 80010 end --print("Go south evaluation:", value) return value end function my_ai:go_south_execution() local units = wesnoth.get_units { side = wesnoth.current.side, formula = "(hitpoints < max_hitpoints) and ($this_unit.moves > 0)" } for i,u in ipairs(units) do ai.move_full(u, u.x, u.y+1) end end ------------------------------------------------- wesnoth.clear_messages() local eval = my_ai:go_south_evaluation() if (eval > 0) then my_ai:go_south_execution() end
The code below the dashed line can be any variation of AI function calls and can contain whatever debug message output is desired.
- Once testing is done and we are happy with the code, all we need to do is copy everything above the dashed line into the engine code. Ideally, we also make ai a local variable there and delete everything not needed from the preload event (including the global ai variable).
Setting up the data Variable
Note that the data variable is not set up in this testing environment, because it gets added by the engine to the local variable my_ai that is not available outside the engine. If data is needed, there are two ways of dealing with this:
- Make my_ai a global variable for testing. This requires setting it up with 'my_ai = {}' in the preload event, and deleting the 'local' in front of my_ai in the engine definition as well as in the file '~add-ons/LuaAI_tests/lua_manual.lua' (or deleting it entirely from the latter).
- Starting the code in file '~add-ons/LuaAI_tests/lua_manual.lua' with
local lurker_ai = {} lurker_ai.data = {}
and thus specifically defining the data variable there.
Both methods include variable definitions that are unnecessary for the final AI code and should be undone for the final version. However, the code will still work even if we forget to undo these definitions.
Behavior (Sticky) Candidate Actions
Behavior (Sticky) Candidate Actions (BCAs) are entirely equivalent to "normal" CAs except that they are specifically tied to a unit. As such, the BCA is removed when the unit dies. This has two advantages:
- The BCA code does not need to include checks whether the unit exists
- No computation time is taken up for evaluating the BCA after the unit dies (although that is likely a very small amount of time saved)
Besides that, BCAs work in exactly the same way as other CAs. In particular, they are also evaluated against the other CAs using the same criteria. Thus, if a BCA is what a unit is supposed to do under all circumstances, it needs to be given a higher evaluation score than all other CAs that might apply to the unit.
As an example, to apply the go-south code from above only to a unit with id 'Rark' on Side 1, we would use:
[add_ai_behavior] side=1 [filter] id="Rark" [/filter] sticky=yes loop_id=main_loop evaluation="return 300000" execution="(...):go_south_execution('Rark')" [/add_ai_behavior]
This declaration needs to be placed inside ActionWML after the unit has been created. In the code, 'sticky' means that the candidate action is tied to the unit defined by [filter]. 'loop_id' needs to be the id of the RCA AI stage. Usually that is main_loop. The evaluation score is hard-coded to 300,000 in order for the BCA to be executed before all other CAs. 'execution' calls my_ai:go_south_execution() in the same way as before, but now using parameter 'Rark'. The execution function then needs to be:
function my_ai:go_south_execution(id) local unit = wesnoth.get_units { id = id }[1] ai.move_full(unit, unit.x, unit.y+1) end
If go_south_execution(id) is only to be done when Rark is wounded, an evaluation function similar to the previous version can be used. Then, Rark would only go south if wounded, and partake in normal AI behavior otherwise, entirely equivalent to the previous example.
An interesting variation of BCA use could be a candidate action that applies to all units of the AI side, but is only executed if a certain unit is on the map. For example, a BCA could be tied to an evil sorcerer leader that makes all his minions attack suicidally without doing anything else. Once the sorcerer is defeated, the BCA disappears with him and the other units on his side start following the standard AI behavior again.
Using the Lua Stage
If the candidate action evaluation mechanism of the RCA AI stage is not needed, Lua AI moves can also be programmed in separate Lua stages, either in place of or in addition to the RCA AI stage:
[stage] engine=lua id=some_id name=some_name code=<< Lua stage code >> [/stage]
The code can be defined directly here, or included from a file using wesnoth.dofile. However, since the Lua engine needs to be defined in any case (in the same way as for the previous example), the AI moves can just as well be defined again in the [engine] tag in exactly the same way as before. They can then be called from the Lua stages entirely analogous to what was done before:
[stage] engine=lua id=some_id name=some_name code=<< (...):do_moves() >> [/stage]
where we assume that a my_ai:do_moves() function is defined in the engine. In that case, the only difference is that the evaluation functions are now not separated from the execution functions on the stage/candidate action level, but that both are taken care of by the do_moves() function.
A Few More Useful Tidbits
- Setting up aspects and goals/targets is described at LuaAI and not repeated here
- debug_utils from Wesnoth Lua Pack is a very powerful tool to analyze what is going on when something is not working as expected. It can be used to display the content of essentially any Wesnoth Lua data structure. As an example, the following command:
debug_utils.dbms(ai, false, "variable", false)
shows all the default Lua AI functions -- assuming that the ai variable has been initialized.
See also: Wesnoth_AI