Skip to content

Instantly share code, notes, and snippets.

@victorjonsson
Last active October 6, 2024 07:46
Show Gist options
  • Save victorjonsson/7c1d6b03da01121859c7155596e287ab to your computer and use it in GitHub Desktop.
Save victorjonsson/7c1d6b03da01121859c7155596e287ab to your computer and use it in GitHub Desktop.
Thread safe and generic state machine, that can be made infinite and guards against invalid transisitions
namespace TestingStuff
{
/// <summary>
/// Thread safe and generic state machine, that can be made infinite and guards against invalid state transisitions
///
/// public enum MyStates {Init, Running, Stopped}
/// var finiteStateMachine = new StateMachine<MyStates>();
///
/// // Infinite state machine that can go back to running or init state once stopped
/// var infiniteStateMachine = new StateMachine<MyStates>(new Transition[] {
/// new Transition(MyState.Init),
/// new Transition(MyState.Running),
/// new Transition(MyState.Stopped, new object[] {MyState.Init, MyState.Running}),
/// });
///
/// // Infinite state machine, allowing any kind of transitions
/// var infiniteStateMachine = new StateMachine<MyState>(allowAnyTransitions: true);
///
/// // More complex
/// public enum Connection = {NotConnected, Connecting, Connected, Disconnecting}
/// var infiniteStateMachine = new StateMachine<MyStates>(new Transition[] {
/// new Transition(Connection.NotConnected, new object[] { Connection.Connecting, Connection.Connected }),
/// new Transition(Connection.Connecting, new object[] { Connection.NotConnected, Connection.Connected }),
/// new Transition(Connection.Connected, new object[] { "*" }), // can turn into any state once connected
/// new Transition(Connection.Disconnecting, new object[] { Connection.NotConnected }),
/// });
///
///
/// </summary>
/// <typeparam name="T"></typeparam>
public class StateMachine<T> where T : Enum
{
private readonly Transition<T>[] _transistions;
private readonly bool _allTransitionsAllowed = false;
private object _lock = new object();
private int _currentStateIndex;
public StateMachine(bool allowAnyTransitions, T initialState = default)
{
var canTransitionTo = allowAnyTransitions ? new object[] { "*" } : Array.Empty<object>();
_transistions = CreateTransitionsArray(canTransitionTo);
_currentStateIndex = FindStateIndex(initialState);
_allTransitionsAllowed = allowAnyTransitions;
}
public StateMachine(T initialState = default)
{
_transistions = CreateTransitionsArray(Array.Empty<object>());
_currentStateIndex = FindStateIndex(initialState);
}
public StateMachine(Transition<T>[] transistions, T initialState = default)
{
_transistions = transistions;
_currentStateIndex = FindStateIndex(initialState);
_allTransitionsAllowed = transistions.All(t => t.ToString() == "*");
}
public T State => _transistions[_currentStateIndex].State;
/// <summary>
/// Transition to new state, will throw InvalidStateTransitionException if not allowed
/// </summary>
/// <param name="newState"></param>
/// <exception cref="InvalidStateTransitionException"></exception>
public void TransitionTo(T newState)
{
lock(_lock)
{
GuardAgainstInvalidStateTransition(newState);
_currentStateIndex = FindStateIndex(newState);
}
}
private void GuardAgainstInvalidStateTransition(T newState)
{
var current = _transistions[_currentStateIndex];
if (current.State.Equals(newState))
{
throw new InvalidStateTransitionException($"Transitioning to {newState} but object is already in this state");
}
if (_allTransitionsAllowed)
{
return;
}
if (current.canTransitionTo == null || current.canTransitionTo.Length == 0)
{
// no rules specified, see that next state is newState
var next = _transistions.Length > (_currentStateIndex + 1) ? _transistions[_currentStateIndex + 1] : null;
if (next == null)
{
throw new InvalidStateTransitionException($"A transition attempt is made to '{newState}' after that the finite state machine has reached its final state ('{current}')");
}
if (!next.State.Equals(newState))
{
throw new InvalidStateTransitionException(current.State, newState, new string[] { next.State.ToString() });
}
}
else if (!current.canTransitionTo.Any(allowedNewState => allowedNewState.ToString() == newState.ToString()))
{
throw new InvalidStateTransitionException(current.State, newState, current.canTransitionTo.Select(s => s.ToString()).ToArray());
}
}
private Transition<T>[] CreateTransitionsArray(object[] canTransitionTo)
{
return Enum.GetValues(typeof(T))
.Cast<T>()
.Select(v => new Transition<T>(v, canTransitionTo))
.ToArray();
}
private int FindStateIndex(T state)
{
return _transistions
.Select((transition, index) => (state: transition.State, index))
.First(tuple => tuple.state.Equals(state)).index;
}
}
public record Transition<T>(T State, object[]? canTransitionTo = null);
public class InvalidStateTransitionException: Exception
{
public InvalidStateTransitionException(string message) : base(message) { }
public InvalidStateTransitionException(object from, object to, object[] allowed)
: base($"Invalid state transition from {from} to {to}, only '{string.Join(", ", allowed.Select(o => o.ToString()))}' allowed") { }
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment