A simple finite-state machine implementation for Lua. Based on @jakesgordon's javascript-finite-state-machine
.
You can install through LuaRocks:
$ luarocks install fsm
Or just download manually:
$ wget https://github.com/unindented/lua-fsm/raw/master/src/fsm.lua
You can create a new state machine by doing something like:
local fsm = require "fsm"
local alert = fsm.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" }
}
})
This will create an object with a method for each event:
alert.warn()
- causes the machine to transition fromgreen
toyellow
alert.panic()
- causes the machine to transition fromyellow
tored
alert.calm()
- causes the machine to transition fromred
toyellow
alert.clear()
- causes the machine to transition fromyellow
togreen
along with the following:
alert.current
- contains the current statealert.is(s)
- returnstrue
if states
is the current statealert.can(e)
- returnstrue
if evente
can be fired in the current statealert.cannot(e)
- returnstrue
if evente
cannot be fired in the current statealert.transitions()
- returns the list of events that are allowed from the current state
If you don't specify any initial state, the state machine will be in the none
state, and you would need to provide an event to take it out of this state:
local alert = fsm.create({
events = {
{name = "startup", from = "none", to = "green"},
{name = "panic", from = "green", to = "red" },
{name = "calm", from = "red", to = "green"}
}
})
print(alert.current) -- "none"
alert.startup()
print(alert.current) -- "green"
If you specify the name of your initial state, then an implicit startup
event will be created for you and fired when the state machine is constructed:
local alert = fsm.create({
initial = "green",
events = {
{name = "panic", from = "green", to = "red" },
{name = "calm", from = "red", to = "green"}
}
})
print(alert.current) -- "green"
If your object already has a startup
method you can use a different name for the initial event:
local alert = fsm.create({
initial = {state = "green", event = "init"},
events = {
{name = "panic", from = "green", to = "red" },
{name = "calm", from = "red", to = "green"}
}
})
print(alert.current) -- "green"
Finally, if you want to wait to call the initial state transition event until a later date you can defer it:
local alert = fsm.create({
initial = {state = "green", event = "init", defer = true},
events = {
{name = "panic", from = "green", to = "red" },
{name = "calm", from = "red", to = "green"}
}
})
print(alert.current) -- "none"
alert.init()
print(alert.current) -- "green"
Four types of callback are available by attaching methods to your state machine, using the following naming conventions (where <EVENT>
and <STATE>
get replaced with your different event and state names):
on_before_<EVENT>
- fired before the eventon_leave_<STATE>
- fired when leaving the old stateon_enter_<STATE>
- fired when entering the new stateon_after_<EVENT>
- fired after the event
For convenience, the 2 most useful callbacks can be shortened:
on_<EVENT>
- convenience shorthand foron_after_<EVENT>
on_<STATE>
- convenience shorthand foron_enter_<STATE>
In addition, four general-purpose callbacks can be used to capture all event and state changes:
on_before_event
- fired before any eventon_leave_state
- fired when leaving any stateon_enter_state
- fired when entering any stateon_after_event
- fired after any event
All callbacks will be passed the same arguments:
- self (the finite-state machine that generated the transition)
- event name
- from state
- to state
- (followed by any arguments you passed into the original event method)
Callbacks can be specified when the state machine is first created:
local fsm = require "fsm"
local alert = fsm.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" }
},
callbacks = {
on_panic = function(self, event, from, to, msg) print('panic! ' .. msg) end,
on_clear = function(self, event, from, to, msg) print('phew... ' .. msg) end
}
})
alert.warn()
alert.panic('killer bees')
alert.calm()
alert.clear('they are gone now')
The order in which callbacks occur is as follows, assuming event calm
transitions from red
state to yellow
:
on_before_calm
- specific handler for the calm event onlyon_before_event
- generic handler for all eventson_leave_red
- specific handler for the red state onlyon_leave_state
- generic handler for all stateson_enter_yellow
- specific handler for the yellow state onlyon_enter_state
- generic handler for all stateson_after_calm
- specific handler for the calm event onlyon_after_event
- generic handler for all events
You may need to execute additional code during a state transition, and ensure the new state is not entered until your code has completed, e.g. fading a menu screen.
One way to do this is to return fsm.DEFERRED
from your on_leave_state
handler, and the state machine will be put on hold until you are ready to confirm the transition by calling the confirm
method, or cancel it by calling the cancel
method:
local screens = fsm.create({
initial = "menu",
events = {
{name = "play", from = "menu", to = "game"},
{name = "quit", from = "game", to = "menu"}
},
callbacks = {
on_leave_menu = function (self)
fade_out(0.5, self.confirm)
return fsm.DEFERRED
end
}
})
screens.play()
- Code:
git clone git://github.com/unindented/lua-fsm.git
- Home: https://github.com/unindented/lua-fsm/
- Daniel Perez Alvarez (unindented@gmail.com)
Copyright (c) 2016 Daniel Perez Alvarez (unindented.org). This is free software, and may be redistributed under the terms specified in the LICENSE file.