Event driven state machine

Posted by FxF on Mon 25 May 2009 09:55 AM — 5 posts, 21,718 views.

#0
Hello,
I have found out that very often I want particular trigger to work only in certain context (depending on previous lines). As a solution to this I track the state by a number of flags (as in_combat=1, bashed=0, skill_lag=1 ... etc.) but the whole script soon starts to be quite complex and hard to understand.

Maybe better approach would be finite state machine paradigm where the script would track current "state". Each state would have it's own triggers, aliases, timers and actions. Depending on mud input (trigger), user output (alias) or time elapsed (timer), the state might change to another state with different sets of triggers, aliases etc.

The state change should include:
  1. Removal (or disabling - I'm not sure which is better) of old triggers, timers, aliases
  2. Definition (or enabling) of new triggers, timers and aliases
  3. Execution of predefined actions


This seems to be extremely powerful approach which would enable to easily create context sensitive triggers, e.g. empty line can be triggered only in combat to separate the combat rounds, mobs in room can be parsed only after the description of room arrived or after recent look or movement input from player.

I'm not experienced with Lua programming and any suggestion how to implement such a state machine in Lua would help. I guess it would involve using the tables but I can't figure out how to do it elegantly.
Australia Forum Administrator #1
One approach that might help is to use the EnableGroup function to enable and disable whole groups of triggers, aliases and timers.

http://www.gammon.com.au/scripts/doc.php?function=EnableGroup

Effectively each group could be your state in your state machine.
#2
I have created first draft of the state machine. For the comparison, the example from the beginning of http://www.gammon.com.au/forum/?id=4957 is used as the test case, perhaps it might be useful for scripting languages without coroutines.

The code is still not complete because it lacks an implementation of functions create_all, delete_all and enable_all, disable_all. It also doesn't solve the problem of passing of the parameters among different states - for example the target of heal from the first alias should be know to the trigger which casts heal.

I could again keep all created triggers, aliases as well as all potential arguments in tables, but I feel that maybe Lua can offer some kind of more object oriented approach which would be easier to work with. Also note that the first part of code contains simple description of particular state machine and everything in the lower part is general to all prospective state machines (there can be more than one machine running). I would like to have this part in separate file which would be required by all state machines, but I'm not sure how to pass "states" variable.

Please let me know if you have any idea how to create some 'object' which would track triggers, aliases and their actual arguments. Is passing the arguments by global table really the best solution?


state_basic = 
{
	aliases = {{"^heal ", jump(state_prepare_heal)}}
}

state_prepare_heal = 
{
	actions = {send("prepare heal")}
	triggers = 
	{
		{"You are ready", jump(state_cast_heal)},
		{"You lose your concentrations", jump(state_prepare_heal)}
	}
}

state_cast_heal =
{
	actions = {send("cast heal")}
	triggers = 
	{
		{"You heal", jump(state_done)},
		{"The spell fizzles out", jump(state_cast_heal)},
	}

state_done = 
{
	actions = {Note("heal done!"), jump(state_basic)}
}


states = {state_basic, state_prepare_heal, state_cast_heal, state_done}

-----------------------------------------------------------------------------
----------------------- state machine description is above ------------------
----------------------- what follows is general for all possible plugins ----
-----------------------------------------------------------------------------

function jump(new_state)
	disable_all(current_state) -- disable old aliases, triggers and timers
	current_state=new_state
	enable_all(current_state)  -- enable new aliases, triggers and timers
	if current_state.actions then
		for k, v in ipairs(current_state.actions) do
			v
		end
	end
end

-- some initialization stuff

current_state=nil

function OnPluginEnable()
	current_state=state_basic
	for k,v in pairs(states) do
		create_all(v) -- this should create all aliases, triggers and timers and keep them in some sort of table
	end
end

function OnPluginDisable()
	for k, v in pairs(states) do
		delete_all(v) -- this will delete all aliases, triggers and timers
	end
end
Australia Forum Administrator #3
Your idea looks ingenious, but I can't help thinking it will bog down in implementation detail. For one thing, state machines often consider more than one condition. As an example, you might transition from fighting to healing oneself if a number of conditions are true:

  • You are fighting
  • You need a heal
  • You have sufficient mana to heal yourself
  • Someone else in your party doesn't need healing more urgently
  • Healing is possible right now (eg. heal spell not on cooldown, you are not stunned, etc.)


Trying to put all that into a state table might be tricky.
#4
I think that things might get complicated simply because they are hard to automate. Healing in combat can be quite complex thing, replacing human decision by a script might not be the best idea.

But in principle it is possible to create script that would decide whether should I heal myself, heal tank, cast lightning bolt or flee. And such script can contain lots of things including random number generator, mood tracker or check whether the tank is my spouse :). And I also believe that the state machine approach would work and the resulting script, while still complex, could be less complex than anything else.

Important feature of my suggested approach is that in the state machine table, the functions called by triggers and aliases don't have to be only 'jump' functions but any functions are allowed. This includes functions that would execute conditional jumps based on other conditions. Yes, it means comeback of various flags and variables tracking all aspects of world (like current spellpoints, wait states, hitpoints, etc.), but I hope that result will be more organized and easier to code and improve.

I'm learning Lua features that would allow using more object oriented approach and I will try to post improved version soon.