batteries/state_machine.lua

184 lines
4.8 KiB
Lua
Raw Normal View History

2020-01-29 03:26:28 +00:00
--[[
state machine
a finite state machine implementation
2020-01-29 03:26:28 +00:00
each state is either:
- a table with enter, exit, update and draw callbacks (all optional)
which each take the state table and varargs as arguments
- a plain function
which gets passed the current event name, the machine table, and varargs as arguments
on changing state, the outgoing state's exit callback is called
then the incoming state's enter callback is called
enter can trigger another transition by returning a string
2020-01-29 03:26:28 +00:00
on update, the current state's update callback is called
the return value can trigger a transition
2020-01-29 03:26:28 +00:00
on draw, the current state's draw callback is called
the return value is discarded
2020-01-29 03:26:28 +00:00
TODO: consider coroutine friendliness
]]
local path = (...):gsub("state_machine", "")
local class = require(path .. "class")
2020-01-29 03:26:28 +00:00
local state_machine = class({
name = "state_machine",
})
2020-01-29 03:26:28 +00:00
function state_machine:new(states, start_in_state)
self.states = states or {}
self.current_state_name = ""
2023-12-18 04:38:30 +00:00
self.prev_state_name = ""
self.reset_state_name = start_in_state or ""
self:reset()
2020-01-29 03:26:28 +00:00
end
--get the current state table (or nil if it doesn't exist)
function state_machine:current_state()
return self.states[self.current_state_name]
end
2020-01-29 03:26:28 +00:00
-------------------------------------------------------------------------------
--internal helpers
--make an internal call
function state_machine:_call(name, ...)
local state = self:current_state()
if state then
if type(state[name]) == "function" then
return state[name](state, ...)
elseif type(state) == "function" then
return state(name, self, ...)
end
2020-01-29 03:26:28 +00:00
end
return nil
end
--make an internal call
-- transition if the return value is a valid state name - and return nil if so
-- return the call result if it isn't a valid state name
function state_machine:_call_and_transition(name, ...)
local r = self:_call(name, ...)
if type(r) == "string" and self:has_state(r) then
2024-10-03 07:12:41 +00:00
self:set_state(r, true)
return nil
end
return r
end
2020-01-29 03:26:28 +00:00
-------------------------------------------------------------------------------
--various checks
function state_machine:in_state(name)
return self.current_state_name == name
2020-01-29 03:26:28 +00:00
end
function state_machine:has_state(name)
return self.states[name] ~= nil
end
-------------------------------------------------------------------------------
--state management
2020-01-29 03:26:28 +00:00
--add a state
function state_machine:add_state(name, state)
if self:has_state(name) then
2023-12-18 04:38:30 +00:00
error("error: added duplicate state " .. name)
2020-01-29 03:26:28 +00:00
else
self.states[name] = state
2020-01-29 03:26:28 +00:00
if self:in_state(name) then
self:_call_and_transition("enter")
2020-01-29 03:26:28 +00:00
end
end
return self
end
--remove a state
function state_machine:remove_state(name)
if not self:has_state(name) then
2023-12-18 04:38:30 +00:00
error("error: removed missing state " .. name)
2020-01-29 03:26:28 +00:00
else
if self:in_state(name) then
self:_call("exit")
end
self.states[name] = nil
end
return self
end
--hard-replace a state table
-- if we're replacing the current state,
-- exit is called on the old state and enter is called on the new state
-- mask_transitions can be used to prevent this if you need to
function state_machine:replace_state(name, state, mask_transitions)
local do_transitions = not mask_transitions and self:in_state(name)
if do_transitions then
2020-01-29 03:26:28 +00:00
self:_call("exit")
end
self.states[name] = state
if do_transitions then
self:_call_and_transition("enter", self)
2020-01-29 03:26:28 +00:00
end
return self
end
--ensure a state doesn't exist; transition out of it if we're currently in it
2020-01-29 03:26:28 +00:00
function state_machine:clear_state(name)
return self:replace_state(name, nil)
2020-01-29 03:26:28 +00:00
end
-------------------------------------------------------------------------------
--transitions and updates
--reset the machine state to whatever state was specified at creation
function state_machine:reset()
if self.reset_state_name then
self:set_state(self.reset_state_name, true)
end
end
--set the current state
-- if the enter callback of the target state returns a valid state name,
-- then it is transitioned to in turn,
-- and so on until the machine is at rest
function state_machine:set_state(name, reset)
if self.current_state_name ~= name or reset then
2020-01-29 03:26:28 +00:00
self:_call("exit")
2023-12-18 04:38:30 +00:00
self.prev_state_name = self.current_state_name
self.current_state_name = name
self:_call_and_transition("enter", self)
2020-01-29 03:26:28 +00:00
end
return self
end
--perform an update
--pass in an optional delta time, which is passed as an arg to the state functions
--if the state update returns a string, and we have that state
-- then we change state (reset if it's the current state)
-- and return nil
--otherwise, the result is returned
2020-01-29 03:26:28 +00:00
function state_machine:update(dt)
return self:_call_and_transition("update", dt)
2020-01-29 03:26:28 +00:00
end
--draw the current state
2020-01-29 03:26:28 +00:00
function state_machine:draw()
self:_call("draw")
end
--for compatibility when a state machine is nested as a state in another machine
function state_machine:enter(parent)
self.parent = parent
self:reset()
end
return state_machine