├── LICENCE ├── README ├── example.lua ├── fsm.lua └── test_fsm.lua /LICENCE: -------------------------------------------------------------------------------- 1 | LICENCE 2 | 3 | The MIT License 4 | 5 | Copyright (c) 2011 Erik Cornelisse - All Rights Reserved. 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy 8 | of this software and associated documentation files (the "Software"), to deal 9 | in the Software without restriction, including without limitation the rights 10 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | copies of the Software, and to permit persons to whom the Software is 12 | furnished to do so, subject to the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be included in 15 | all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 23 | THE SOFTWARE. -------------------------------------------------------------------------------- /README: -------------------------------------------------------------------------------- 1 | A design pattern for doing finite state machines (FSMs) in Lua 2 | ============================================================== 3 | 4 | Based on a very appreciated contribution of Luiz Henrique de Figueiredo 5 | Original code from http://lua-users.org/wiki/FiniteStateMachine 6 | 7 | The FSM is described with: old_state, event, new_state and action. 8 | One easy way to do this in Lua would be to create a table in exactly 9 | the form above: 10 | 11 | yourStateTransitionTable = { 12 | {state1, event1, state2, action1}, 13 | {state1, event2, state3, action2}, 14 | ... 15 | } 16 | 17 | The function FSM takes the simple syntax above and creates tables 18 | for (state, event) pairs with fields (action, new): 19 | 20 | function FSM(yourStateTransitionTable) 21 | local stt = {} 22 | for _,v in ipairs(t) do 23 | local old, event, new, action = v[1], v[2], v[3], v[4] 24 | if stt[old] == nil then a[old] = {} end 25 | stt[old][event] = {new = new, action = action} 26 | end 27 | return stt 28 | end 29 | 30 | Note that this scheme works for states and events of any type: number, 31 | string, functions, tables, anything. Such is the power of associate arrays. 32 | 33 | However, the double array stt[old][event] caused a problem for event = nil 34 | Instead a single array is used, constructed as stt[state .. SEPARATOR .. event] 35 | Where SEPARATOR is a constant and defined as '.' 36 | 37 | Three special state transitions are added to the original code: 38 | - any state but a specific event 39 | - any event but a specific state 40 | - unknown state-event combination to be used for exception handling 41 | 42 | The any state and event are defined by the ANY constant, defined as '*' 43 | The unknown state-event is defined as the combination of ANY.ANY (*.*) 44 | 45 | A default exception handler for unknown state-event combinations is 46 | provided and therefore a specification a your own exception handling is 47 | optional. 48 | 49 | After creating a new FSM, the initial state is set to the first defined 50 | state in your state transition table. With add(t) and delete(t), new state 51 | transition can be added and removed later. 52 | 53 | A DEBUG-like method called silent is included to prevent wise-guy remarks 54 | about things you shouldn't be doing. 55 | 56 | USAGE EXAMPLES: 57 | ------------------------------------------------------------------------------- 58 | FSM = require "fsm" 59 | 60 | function action1() print("Performing action 1") end 61 | function action2() print("Performing action 2") end 62 | 63 | -- Define your state transitions here 64 | local myStateTransitionTable = { 65 | {"state1", "event1", "state2", action1}, 66 | {"state2", "event2", "state3", action2}, 67 | {"*", "event3", "state2", action1}, -- for any state 68 | {"*", "*", "state2", action2} -- exception handler 69 | } 70 | 71 | -- Create your finite state machine 72 | fsm = FSM.new(myStateTransitionTable) 73 | 74 | -- Use your finite state machine 75 | -- which starts by default with the first defined state 76 | print("Current FSM state: " .. fsm:get()) 77 | 78 | -- Or you can set another state 79 | fsm:set("state2") 80 | print("Current FSM state: " .. fsm:get()) 81 | 82 | -- Resond on "event" and last set "state" 83 | fsm:fire("event2") 84 | print("Current FSM state: " .. fsm:get()) 85 | 86 | Output: 87 | ------- 88 | Current FSM state: state1 89 | Current FSM state: state2 90 | Performing action 2 91 | Current FSM state: state3 92 | 93 | REMARKS: 94 | ------------------------------------------------------------------------------- 95 | Sub-states are not supported by additional methods to keep the code simple. 96 | If you need sub-states, you can create them as 'state.substate' directly. 97 | 98 | A specific remove method is not provided because I didn't need one (I think) 99 | But feel free to implement one yourself :-) 100 | 101 | One more thing, did I already mentioned that I am new to Lua? 102 | Well, I learn a lot of other examples, so do not forget yours. 103 | 104 | CONCLUSION: 105 | ------------------------------------------------------------------------------- 106 | A small amount of code is required to use the power of a Finite State Machine. 107 | 108 | I wrote this code to implement generic graphical objects where the presentation 109 | and behaviour of the objects can be specified by using state-transition tables. 110 | This resulted in less code (and less bugs), a higher modularity and above all a 111 | reduction of the complexity. 112 | 113 | Finite state machines can be used to force (at least parts of) your code into 114 | deterministic behaviour. 115 | 116 | Have fun. -------------------------------------------------------------------------------- /example.lua: -------------------------------------------------------------------------------- 1 | -- 2 | -- Test & demonstration file for "fsm.lua" 3 | -- 4 | 5 | FSM = require "fsm" 6 | 7 | function action1() print("Performing action 1") end 8 | 9 | function action2() print("Performing action 2") end 10 | 11 | function action3() print("Action 3: Exception raised") end 12 | 13 | function action4() print("Wildcard in action !!!") end 14 | 15 | -- Define your state transitions here 16 | local myStateTransitionTable = { 17 | {"state1", "event1", "state2", action1}, 18 | {"state2", "event2", "state3", action2} 19 | } 20 | 21 | -- Create your instance of a finite state machine 22 | fsm = FSM.new(myStateTransitionTable) 23 | 24 | -- print( "Constant UNKNOWN = " .. UNKNOWN ) 25 | print( "Constant FSM.UNKNOWN = " .. FSM.UNKNOWN ) 26 | 27 | -- Use your finite state machine 28 | -- which starts by default with the first defined state 29 | print("Current FSM state: " .. fsm:get()) 30 | 31 | -- Or you can set another state 32 | fsm:set("state2") 33 | print("Current FSM state: " .. fsm:get()) 34 | 35 | -- Respond on "event" and last set "state" 36 | fsm:fire("event2") 37 | print("Current FSM state: " .. fsm:get()) 38 | 39 | -- Force exception 40 | print("Force exception by firing unknown event") 41 | fsm:fire("event3") 42 | print("Current FSM state: " .. fsm:get()) 43 | 44 | -- Test automated exception handling 45 | local myStateTransitionTable2 = { 46 | {"state1", "event1", "state2", action1}, 47 | {"state2", "event2", "state3", action2}, 48 | {"*" , "event3", "state1", action4}, 49 | {"*" , "?", "state1", action3} 50 | } 51 | 52 | -- Create your instance of a finite state machine 53 | fsm2 = FSM.new(myStateTransitionTable2) 54 | fsm2:set("state2") 55 | print("\nCurrent FSM-2 state: " .. fsm2:get()) 56 | 57 | fsm2:delete({{"state2", "event2"}} ) 58 | -- Force exception 59 | fsm2:fire("event2") 60 | 61 | fsm2:delete({{"*", "?"}} ) 62 | print("Force third exception (silence = true)") 63 | fsm2:silent() -- prevent unknown state-event notificaton to be printed 64 | 65 | fsm2:fire("event3") 66 | print("Current FSM-2 state after firing wildcard 'event3': " .. fsm2:get()) 67 | 68 | fsm2:add({{"*" , "*", "state2", action3}}) 69 | fsm2:fire("event2") 70 | print("Current FSM-2 state: " .. fsm2:get()) 71 | print("Current FSM state: " .. fsm:get()) 72 | 73 | --[[ 74 | Expected output: 75 | 76 | Constant FSM.UNKNONW = *.* 77 | Current FSM state: state1 78 | Current FSM state: state2 79 | Performing action 2 80 | Current FSM state: state3 81 | Force exception by firing unknown event 82 | FSM: unknown combination: state3.event3 83 | Current FSM state: state1 84 | 85 | Current FSM-2 state: state2 86 | FSM: unknown combination: state2.event2 87 | Force third exception (silence = true) 88 | Wildcard in action !!! 89 | Current FSM-2 state after firing wildcard 'event3': state1 90 | Action 3: Exception raised 91 | Current FSM-2 state: state2 92 | Current FSM state: state1 93 | --]] -------------------------------------------------------------------------------- /fsm.lua: -------------------------------------------------------------------------------- 1 | -- ========================================================================================== 2 | -- 3 | -- Finite State Machine Class for Lua 5.1 & Corona SDK 4 | -- 5 | -- Written by Erik Cornelisse, inspired by Luiz Henrique de Figueiredo 6 | -- E-mail: e.cornelisse@gmail.com 7 | -- 8 | -- Version 1.0 April 27, 2011 9 | -- 10 | -- Class is MIT Licensed 11 | -- Copyright (c) 2011 Erik Cornelisse 12 | -- 13 | -- Feel free to change but please send new versions, improvements or 14 | -- new features to me and help us to make it better. 15 | -- 16 | -- ========================================================================================== 17 | 18 | --[[ A design pattern for doing finite state machines (FSMs) in Lua. 19 | 20 | Based on a very appreciated contribution of Luiz Henrique de Figueiredo 21 | Original code from http://lua-users.org/wiki/FiniteStateMachine 22 | 23 | The FSM is described with: old_state, event, new_state and action. 24 | One easy way to do this in Lua would be to create a table in exactly 25 | the form above: 26 | 27 | yourStateTransitionTable = { 28 | {state1, event1, state2, action1}, 29 | {state1, event2, state3, action2}, 30 | ... 31 | } 32 | 33 | The function FSM takes the simple syntax above and creates tables 34 | for (state, event) pairs with fields (action, new): 35 | 36 | function FSM(yourStateTransitionTable) 37 | local stt = {} 38 | for _,v in ipairs(t) do 39 | local old, event, new, action = v[1], v[2], v[3], v[4] 40 | if stt[old] == nil then a[old] = {} end 41 | stt[old][event] = {new = new, action = action} 42 | end 43 | return stt 44 | end 45 | 46 | Note that this scheme works for states and events of any type: number, 47 | string, functions, tables, anything. Such is the power of associate arrays. 48 | 49 | However, the double array stt[old][event] caused a problem for event = nil 50 | Instead a single array is used, constructed as stt[state .. SEPARATOR .. event] 51 | Where SEPARATOR is a constant and defined as '.' 52 | 53 | Three special state transitions are added to the original code: 54 | - any state but a specific event 55 | - any event but a specific state 56 | - unknown state-event combination to be used for exception handling 57 | 58 | The any state and event are defined by the ANY constant, defined as '*' 59 | The unknown state-event is defined as the combination of ANY.ANY (*.*) 60 | 61 | A default exception handler for unknown state-event combinations is 62 | provided and therefore a specification a your own exception handling is 63 | optional. 64 | 65 | After creating a new FSM, the initial state is set to the first defined 66 | state in your state transition table. With add(t) and delete(t), new state 67 | transition can be added and removed later. 68 | 69 | A DEBUG-like method called silent is included to prevent wise-guy remarks 70 | about things you shouldn't be doing. 71 | 72 | USAGE EXAMPLES: 73 | ------------------------------------------------------------------------------- 74 | FSM = require "fsm" 75 | 76 | function action1() print("Performing action 1") end 77 | function action2() print("Performing action 2") end 78 | 79 | -- Define your state transitions here 80 | local myStateTransitionTable = { 81 | {"state1", "event1", "state2", action1}, 82 | {"state2", "event2", "state3", action2}, 83 | {"*", "event3", "state2", action1}, -- for any state 84 | {"*", "*", "state2", action2} -- exception handler 85 | } 86 | 87 | -- Create your finite state machine 88 | fsm = FSM.new(myStateTransitionTable) 89 | 90 | -- Use your finite state machine 91 | -- which starts by default with the first defined state 92 | print("Current FSM state: " .. fsm:get()) 93 | 94 | -- Or you can set another state 95 | fsm:set("state2") 96 | print("Current FSM state: " .. fsm:get()) 97 | 98 | -- Resond on "event" and last set "state" 99 | fsm:fire("event2") 100 | print("Current FSM state: " .. fsm:get()) 101 | 102 | Output: 103 | ------- 104 | Current FSM state: state1 105 | Current FSM state: state2 106 | Performing action 2 107 | Current FSM state: state3 108 | 109 | REMARKS: 110 | ------------------------------------------------------------------------------- 111 | Sub-states are not supported by additional methods to keep the code simple. 112 | If you need sub-states, you can create them as 'state.substate' directly. 113 | 114 | A specific remove method is not provided because I didn't need one (I think) 115 | But feel free to implement one yourself :-) 116 | 117 | One more thing, did I already mentioned that I am new to Lua? 118 | Well, I learn a lot of other examples, so do not forget yours. 119 | 120 | CONCLUSION: 121 | ------------------------------------------------------------------------------- 122 | A small amount of code is required to use the power of a Finite State Machine. 123 | 124 | I wrote this code to implement generic graphical objects where the presentation 125 | and behaviour of the objects can be specified by using state-transition tables. 126 | This resulted in less code (and less bugs), a higher modularity and above all a 127 | reduction of the complexity. 128 | 129 | Finite state machines can be used to force (at least parts of) your code into 130 | deterministic behaviour. 131 | 132 | Have fun !! 133 | 134 | --]] 135 | 136 | --MODULE CODE 137 | ------------------------------------------------------------------------------- 138 | module(..., package.seeall) 139 | 140 | -- FSM CONSTANTS -------------------------------------------------------------- 141 | SEPARATOR = '.' 142 | ANY = '*' 143 | ANYSTATE = ANY .. SEPARATOR 144 | ANYEVENT = SEPARATOR .. ANY 145 | UNKNOWN = ANYSTATE .. ANY 146 | 147 | function new(t) 148 | 149 | local self = {} 150 | 151 | -- ATTRIBUTES ------------------------------------------------------------- 152 | local state = t[1][1] -- current state, default the first one 153 | local stt = {} -- state transition table 154 | local str = "" -- combination 155 | local silence = false -- use silent() for whisper mode 156 | 157 | -- METHODS ---------------------------------------------------------------- 158 | 159 | -- some getters and setters 160 | function self:set(s) state = s end 161 | function self:get() return state end 162 | function self:silent() silence = true end 163 | 164 | -- default exception handling 165 | local function exception() 166 | if silence == false then 167 | print("FSM: unknown combination: " .. str) end 168 | return false 169 | end 170 | 171 | -- respond based on current state and event 172 | function self:fire(event) 173 | local act = stt[state .. SEPARATOR .. event] 174 | -- raise exception for unknown state-event combination 175 | if act == nil then 176 | -- search for wildcard "states" for this event 177 | act = stt[ANYSTATE .. event] 178 | -- if still not a match than check any event 179 | if act == nil then 180 | -- check if there is a wildcard event 181 | act = stt[state .. ANYEVENT] 182 | if act == nil then 183 | act = stt[UNKNOWN]; str = state .. SEPARATOR .. event end 184 | end 185 | end 186 | -- set next state as current state 187 | state = act.newState 188 | 189 | return act.action() 190 | end 191 | 192 | -- add new state transitions to the FSM 193 | function self:add(t) 194 | for _,v in ipairs(t) do 195 | local oldState, event, newState, action = v[1], v[2], v[3], v[4] 196 | 197 | stt[oldState .. SEPARATOR .. event] = {newState = newState, action = action} 198 | end 199 | return #t -- the requested number of state-transitions to be added 200 | end 201 | 202 | -- remove state transitions from the FSM 203 | function self:delete(t) 204 | for _,v in ipairs(t) do 205 | local oldState, event = v[1], v[2] 206 | if oldState == ANY and event == ANY then 207 | if not silence then 208 | print( "FSM: you should not delete the exception handler" ) 209 | print( "FSM: but assign another exception action" ) 210 | end 211 | -- assign default exception handler but stay in current state 212 | stt[exception] = {newState = state, action = exception} 213 | else 214 | stt[oldState .. SEPARATOR .. event] = nil 215 | end 216 | end 217 | return #t -- the requested number of state-transitions to be deleted 218 | end 219 | 220 | -- initalise state transition table 221 | stt[UNKNOWN] = {newState = state, action = exception} 222 | 223 | self:add(t) 224 | 225 | -- return FSM methods 226 | return self 227 | end 228 | 229 | 230 | -------------------------------------------------------------------------------- /test_fsm.lua: -------------------------------------------------------------------------------- 1 | --[[ 2 | File: test_fsm.lua 3 | 4 | Description: Tests for the finite state machine library 5 | 6 | Author: Erik Cornelisse 7 | Version 1.0 April 27, 2011 8 | 9 | ]]-- 10 | 11 | require "luaunit" 12 | FSM = require "fsm" 13 | 14 | function action1() return 1 end -- #1 Performaing action 1 15 | function action2() return 2 end -- #2 Performaing another action 16 | function action3() return 3 end -- #3 Exception raised 17 | function action4() return 4 end -- #4 Wildcard in action 18 | 19 | TestFSM = {} --class 20 | 21 | function TestFSM:setUp() 22 | local myStateTransitionTable1 = { 23 | {"state1", "event1", "state2", action1}, 24 | {"state2", "event2", "state3", action2} 25 | } 26 | 27 | local myStateTransitionTable2 = { 28 | {"state1", "event1", "state2", action1}, 29 | {"state2", "event2", "state3", action2}, 30 | {"*" , "event3", "state1", action4}, 31 | {"*" , "*", "state1", action3} 32 | } 33 | 34 | -- creating two different FSM's to test (lack of) interference 35 | fsm1 = FSM.new(myStateTransitionTable1) 36 | fsm2 = FSM.new(myStateTransitionTable2) 37 | 38 | fsm1:silent() 39 | fsm2:silent() 40 | end 41 | 42 | function TestFSM:test1() 43 | 44 | assertEquals( FSM.UNKNOWN , "*.*" ) 45 | 46 | -- Retrieve initial state 47 | assertEquals( fsm1:get() , "state1" ) 48 | 49 | -- Set another state 50 | fsm1:set( "state2" ) 51 | assertEquals( fsm1:get() , "state2" ) 52 | 53 | -- Respond on "event" and current state 54 | assertEquals( fsm1:fire("event2"), 2) 55 | assertEquals( fsm1:get() , "state3" ) 56 | 57 | -- Force "default" exception for "state3.event3" 58 | assertEquals( fsm1:fire("event3") , false) 59 | assertEquals( fsm1:get() , "state1" ) 60 | end 61 | 62 | function TestFSM:test2() 63 | 64 | -- Retrieve initial state 65 | assertEquals( fsm2:get() , "state1" ) 66 | 67 | -- Set another state 68 | fsm2:set( "state2" ) 69 | assertEquals( fsm2:get() , "state2" ) 70 | 71 | -- Respond on "event" and current state 72 | assertEquals( fsm2:fire("event2"), 2) 73 | assertEquals( fsm2:get() , "state3" ) 74 | 75 | -- Force "wildcard" exception "*.event3" => action #4 76 | assertEquals( fsm2:fire("event3"), 4) 77 | assertEquals( fsm2:get() , "state1" ) 78 | end 79 | 80 | function TestFSM:test3() 81 | 82 | fsm2:set( "state2" ) 83 | assertEquals( fsm2:get() , "state2" ) 84 | 85 | -- Force exception caused by removed state transition 86 | assertEquals( fsm2:delete({{"state2", "event2"}}), 1) 87 | assertEquals( fsm2:fire("event2"), 3 ) 88 | assertEquals( fsm2:get() , "state1" ) 89 | 90 | -- Remove the exception handler 91 | assertEquals( fsm2:delete({{"*", "*"}}), 1) 92 | 93 | -- Correcting our mistake 94 | assertEquals( fsm2:add({{"*", "*", "state2", action3}}), 1) 95 | assertEquals( fsm2:fire("event2"), 3) 96 | 97 | end 98 | -- class TestFSM 99 | 100 | LuaUnit:run() -- will execute all tests --------------------------------------------------------------------------------