import tables
, strutils
, options
type
# entry_action: Option[Callback]
# exit_action: Option[Callback]
State_Actions = tuple[ entry_action: Option[Callback]
, exit_action: Option[Callback]
]
Callback = proc(): void
StateEvent[S,E] = tuple[state: S, event: E]
Transition[S] = tuple[nextState: S, action: Option[Callback]]
State_Machine*[S,E] = ref object of RootObj
initial_state*: S
current_state*: Option[S]
state_actions*: array[S, StateActions]
transitions*: TableRef[StateEvent[S,E], Transition[S]]
free_transitions*: TableRef[S, Transition[S]]
default_transition*: Option[Transition[S]]
#TransitionNotFoundException = object of Exception
proc current_state_is[S,E]( m: State_Machine[S,E]
, nextState: S
) =
if m.current_state.isSome:
if m.state_actions[m.current_state.get].exit_action.isSome:
get(m.state_actions[m.current_state.get].exit_action)()
m.current_state = some(nextState)
if m.state_actions[m.current_state.get].entry_action.isSome:
get(m.state_actions[m.current_state.get].entry_action)()
proc reset*[S,E]( m: State_Machine[S,E]
) =
m.setCurrentState m.initial_state
proc initial_state_is*[S,E]( m: State_Machine[S,E]
, state: S
) =
m.initial_state = state
proc add_state_actions*[S,E]( m: State_Machine[S,E]
, state: S
, entry_action: Callback = nil
, exit_action: Callback = nil
) =
let
entry = if entry_action == nil: none(Callback)
else: some(entry_action)
exit = if exit_action == nil: none(Callback)
else: some(exit_action)
m.state_actions[state] = (entry, exit)
proc a_state_machine*[S,E]( initial_state: S
): State_Machine[S,E] =
result = State_Machine[S,E]()
result.transitions = newTable[ StateEvent[S,E]
, Transition[S]
]()
result.free_transitions = newTable[ S
, Transition[S]
]()
result.initial_state_is initial_state
proc add_free_transition*[S,E]( m: State_Machine[S,E]
, state: S, nextState: S
) =
m.free_transitions[state] = (nextState, none(Callback))
proc addTransitionAny*[S,E](m: State_Machine[S,E], state, nextState: S, action: Callback) =
m.free_transitions[state] = (nextState, some(action))
proc add_transition*[S,E](m: State_Machine[S,E], state: S, event: E, nextState: S) =
m.transitions[(state, event)] = (nextState, none(Callback))
proc add_transition*[S,E](m: State_Machine[S,E], state: S, event: E, nextState: S, action: Callback) =
m.transitions[(state, event)] = (nextState, some(action))
proc setDefaultTransition*[S,E](m: State_Machine[S,E], state: S) =
m.default_transition = some((state, none(Callback)))
proc setDefaultTransition*[S,E](m: State_Machine[S,E], state: S, action: Callback) =
m.default_transition = some((state, some(action)))
proc the_transition*[S,E]( m: State_Machine[S,E]
, event: E
, state: S
): Transition[S] =
let map = (state, event)
if m.transitions.hasKey(map): result = m.transitions[map]
elif m.free_transitions.hasKey(state): result = m.free_transitions[state]
elif m.default_transition.isSome: result = m.default_transition.get
else:
echo map
quit "TransitionNotFoundException"
proc process*[S,E]( m: State_Machine[S,E]
, event: E
) =
let transition = m.the_transition(event, m.current_state.get)
if transition[1].isSome: get(transition[1])()
m.current_state_is transition[0]
#echo event, " ", m.current_state.get
when isMainModule:
type
StateName = enum
SOLID
LIQUID
GAS
PLASMA
Event = enum
MELT
EVAPORATE
SUBLIMATE
IONIZE
var m = newMachine[StateName, Event](LIQUID)
proc cb() =
echo "i'm evaporating"
var condition: bool
proc enterLiquid() =
echo "entering liquid state"
proc exitLiquid() =
echo "exiting liquid state"
proc enterGas() =
if condition:
echo "entering gas state and ionizing immediately"
m.process(IONIZE)
else:
echo "entering gas state"
proc exitGas() =
echo "exiting gas state"
proc enterPlasma() =
echo "entering plasma state"
m.add_transition(SOLID, MELT, LIQUID)
m.add_transition(LIQUID, EVAPORATE, GAS, cb)
m.add_transition(SOLID, SUBLIMATE, GAS)
m.add_transition(GAS, IONIZE, PLASMA)
m.add_transition(SOLID, MELT, LIQUID)
m.addStateActions(LIQUID, entry_action=enterLiquid, exit_action=exitLiquid)
m.addStateActions(PLASMA, enterPlasma)
m.addStateActions(GAS, enterGas, exitGas)
# to "start" the fsm
# this is necessary
m.reset
assert m.getCurrentState() == LIQUID
condition = false
m.process(EVAPORATE)
assert m.getCurrentState() == GAS
m.process(IONIZE)
assert m.getCurrentState() == PLASMA
echo "\nreseting\n"
m.reset
assert m.getCurrentState() == LIQUID
condition = true
m.process(EVAPORATE)
assert m.getCurrentState() == PLASMA, $m.getCurrentState()