statemachine.lua

Showcase your libraries, tools and other projects that help your fellow love users.
Post Reply
User avatar
conroy
Prole
Posts: 46
Joined: Thu May 24, 2012 3:33 pm

statemachine.lua

Post by conroy »

I needed a state machine for my game. All the libraries I found didn't suit my needs, so I wrote my own

https://github.com/kyleconroy/lua-state-machine

Basic example creating a stoplight state machine.

Code: Select all

local machine = require('statemachine')

local fsm = machine.create({
  initial = 'green',
  events = {
    { name = 'warn',  from = 'green',  to = 'yellow' },
    { name = 'panic', from = 'yellow', to = 'red'    },
    { name = 'calm',  from = 'red',    to = 'yellow' },
    { name = 'clear', from = 'yellow', to = 'green'  }
}})

fsm:warn()  -- transition from 'green' to 'yellow'
fsm:panic() -- transition from 'yellow' to 'red'
fsm:calm()  -- transition from 'red' to 'yellow'
fsm:clear() -- transition from 'yellow' to 'green'
User avatar
rokit boy
Party member
Posts: 198
Joined: Wed Jan 18, 2012 7:40 pm

Re: statemachine.lua

Post by rokit boy »

pretty neat, minimal but neat.
u wot m8
User avatar
kikito
Inner party member
Posts: 3153
Joined: Sat Oct 03, 2009 5:22 pm
Location: Madrid, Spain
Contact:

Re: statemachine.lua

Post by kikito »

I gave this a look and it's pretty neat. I like how the initial example is easy to grasp, but then you keep reading and reading and discovering advanced stuff if you need it.

I've got some minor syntax suggestions for you to consider:

1- Using the event name as keys of the "events" table:

So this:

Code: Select all

local fsm = machine.create({
  initial = 'green',
  events = {
    { name = 'warn',  from = 'green',  to = 'yellow' },
    { name = 'panic', from = 'yellow', to = 'red'    },
    { name = 'calm',  from = 'red',    to = 'yellow' },
    { name = 'clear', from = 'yellow', to = 'green'  }
  }
})
Becomes:

Code: Select all

local fsm = machine.create({
  initial = 'green',
  events = {
    warn  = { from = 'green',  to = 'yellow' },
    panic = { from = 'yellow', to = 'red'    },
    calm  = { from = 'red',    to = 'yellow' },
    clear = { from = 'yellow', to = 'green'  }
  }
})
Less typing, same info.

2- Putting the callbacks inside the event table, and remove the "on":

Code: Select all

local fsm = machine.create({
  initial = 'green',
  events = {
    warn  = { from = 'green',  to = 'yellow', enter= function() ... end },
    panic = { from = 'yellow', to = 'red', before=function() ... end    },
    calm  = { from = 'red',    to = 'yellow', after=function() ... end },
    clear = { from = 'yellow', to = 'green', leave=function() ... end  }
  }
})
3- By the way, I'm not sure about the difference between "enter" vs "before" and "after" vs "leave". Maybe adding a phrase saying "The difference between enter and before is ..." right after explaining the callbacks would help.

4- Remove "magical" callback function names
By magical I'm referring to these. They require too much mental work IMHO:

Code: Select all

fsm.ongreen       = nil -- was this on enter, or onBefore? If I change ongreen, what happens if I invoke onentergreen?
fsm.onyellow      = nil
fsm.onred         = nil
fsm.onchangestate = function(event, from, to) print(to) end
If you transform your events into call-able tables, you can have a slightly longer names, but with no magic. I consider this better because there are no rules to remember. Let the code explains itself:

Code: Select all

fsm.green.enter       = nil
fsm.yellow.enter      = nil
fsm.red.enter         = nil
fsm.change = function(event, from, to) print(to) end -- instead of onchangestate
Nice thing: you can define the callbacks using Lua's regular function definition syntax if you want:

Code: Select all

function fsm.yellow.enter(...)
...
end
Having fsm.yellow as a table has other advantages. For example you could define fsm.yellow.draw, fsm.red.draw, and then just do fsm.current.draw().

This is all I could think for now.

Regards!

EDIT: I forgot to mention that I saw it's integrated with TravisCI! That's very cool! But out of curiosity: why "erlang"?
When I write def I mean function.
User avatar
Lafolie
Inner party member
Posts: 809
Joined: Tue Apr 05, 2011 2:59 pm
Location: SR388
Contact:

Re: statemachine.lua

Post by Lafolie »

This is awesome! Great to see kikito's additions too.
Do you recognise when the world won't stop for you? Or when the days don't care what you've got to do? When the weight's too tough to lift up, what do you? Don't let them choose for you, that's on you.
User avatar
Robin
The Omniscient
Posts: 6506
Joined: Fri Feb 20, 2009 4:29 pm
Location: The Netherlands
Contact:

Re: statemachine.lua

Post by Robin »

kikito wrote:1- Using the event name as keys of the "events" table:
I wanted to suggest that as well, but then I saw it wouldn't work:

From an example in his GitHub README:

Code: Select all

local fsm = machine.create({
  initial = 'hungry',
  events = {
    { name = 'eat',  from = 'hungry',                                to = 'satisfied' },
    { name = 'eat',  from = 'satisfied',                             to = 'full'      },
    { name = 'eat',  from = 'full',                                  to = 'sick'      },
    { name = 'rest', from = {'hungry', 'satisfied', 'full', 'sick'}, to = 'hungry'    },
}})
(The original had a bug: it had [] instead of {} for the sequence -- it was translated from JavaScript, which explains that.)

Anyway, it becomes really hairy if you try to use the event name as a key there.

I guess you could do

Code: Select all

local fsm = machine.create({
  initial = 'hungry',
  events = {
    eat = { name = 'eat',  from = {'hungry', 'satisfied', 'full'}, to = {'satisfied', 'full', 'sick'}},
    rest = {from = {'hungry', 'satisfied', 'full', 'sick'}, to = 'hungry'},
}})
Hm, maybe that's not even a bad idea.
Help us help you: attach a .love.
User avatar
conroy
Prole
Posts: 46
Joined: Thu May 24, 2012 3:33 pm

Re: statemachine.lua

Post by conroy »

Yep, Robin pointed out the problem with using a table indexed by event name instead of just a table of tables.

The reason it uses the erlang type is funny. Travis CI doesn't have native support for Lua, so you have to install lua and luajit yourself. The erlang environment makes this easy to do, hence the reason for its use.

I personally like the named state transitions. While "magic" I think they are easy to understand. I think defining all the functions in the same table, per the first example, is a bit too much.

I'm pretty happy with where this is now. Some of the features (such as multiple states and async transitions) don't work yet, so I'll be adding support for those next.
Post Reply

Who is online

Users browsing this forum: No registered users and 1 guest