From afbb15b58d46f6868adeb4c60eb13ea945e8873a Mon Sep 17 00:00:00 2001 From: rlovely Date: Sat, 20 Jan 2024 09:52:02 -0500 Subject: [PATCH] Add optional name, add Delegates to not require (vars), make calling ExitState.AddTransitionTo a compiler error --- Demo/Program.cs | 6 +-- StateMachine/Delegates.cs | 17 ++++++- StateMachine/ExitState.cs | 21 ++++----- StateMachine/FunctionState.cs | 72 ++++++++++++++++++++++++----- StateMachine/State.cs | 38 ++++++++++++++- StateMachine/StateEventArgs.cs | 10 ++++ StateMachine/StateMachine.cs | 18 +++++++- StateMachine/Transition.cs | 12 ++++- UnitTests/FunctionStateUnitTests.cs | 26 +++++------ UnitTests/StateMachineUnitTests.cs | 32 ++++++++++++- UnitTests/StateUnitTests.cs | 48 +++++++++++++++++++ 11 files changed, 253 insertions(+), 47 deletions(-) create mode 100644 StateMachine/StateEventArgs.cs create mode 100644 UnitTests/StateUnitTests.cs diff --git a/Demo/Program.cs b/Demo/Program.cs index 904dfaf..9de1c53 100644 --- a/Demo/Program.cs +++ b/Demo/Program.cs @@ -39,12 +39,12 @@ mainState.AddTransitionTo(oddState, (vars) => vars["x"] % 2 == 1); mainState.AddTransitionTo(evenState, (vars) => vars["x"] % 2 == 0); - oddState.AddTransitionTo(incrementState, null); + oddState.AlwaysTransitionTo(incrementState); - evenState.AddTransitionTo(incrementState, null); + evenState.AlwaysTransitionTo(incrementState); incrementState.AddTransitionTo(StateMachine.ExitState, (vars) => vars["x"] > 10); - incrementState.AddTransitionTo(mainState, null); + incrementState.AlwaysTransitionTo(mainState); classSM.InitialState = mainState; diff --git a/StateMachine/Delegates.cs b/StateMachine/Delegates.cs index b49e2eb..9b7073b 100644 --- a/StateMachine/Delegates.cs +++ b/StateMachine/Delegates.cs @@ -1,15 +1,28 @@ namespace RoadieRichStateMachine { + /// + /// Determines whether to take a certain transition + /// + /// true if the transition should be taken + public delegate bool TransitionConditionDelegate(); + /// /// Determines whether to take a certain transition /// /// A dictionary of variables shared between all states and transitions /// true if the transition should be taken - public delegate bool TransitionConditionDelegate(IDictionary vars); + public delegate bool TransitionConditionDelegateWithVars(IDictionary vars); + + /// + /// A function run by + /// + /// A dictionary of variables shared between all states and transitions + public delegate void FunctionStateFunctionDelegate(); + /// /// A function run by /// /// A dictionary of variables shared between all states and transitions - public delegate void FunctionStateFunctionDelegate(IDictionary vars); + public delegate void FunctionStateFunctionDelegateWithVars(IDictionary vars); } diff --git a/StateMachine/ExitState.cs b/StateMachine/ExitState.cs index 0a785b2..e6e5059 100644 --- a/StateMachine/ExitState.cs +++ b/StateMachine/ExitState.cs @@ -2,19 +2,16 @@ { public sealed class ExitState : State { - protected override void Enter(IDictionary vars) - { - throw new NotImplementedException(); - } + internal ExitState() : base("Exit State") { } - protected override void Exit(IDictionary vars) - { - throw new NotImplementedException(); - } + protected override void Enter(IDictionary vars) => throw new NotImplementedException(); - protected override void Inner(IDictionary vars) - { - throw new NotImplementedException(); - } + protected override void Exit(IDictionary vars) => throw new NotImplementedException(); + + protected override void Inner(IDictionary vars) => throw new NotImplementedException(); + + [Obsolete("You cannot add transitions to ExitState", true)] public new State AddTransitionTo(State to, TransitionConditionDelegate condition) => throw new InvalidOperationException(); + [Obsolete("You cannot add transitions to ExitState", true)] public new State AddTransitionTo(State to, TransitionConditionDelegateWithVars condition) => throw new InvalidOperationException(); + [Obsolete("You cannot add transitions to ExitState", true)] public new State AlwaysTransitionTo(State to) => throw new InvalidOperationException(); } } diff --git a/StateMachine/FunctionState.cs b/StateMachine/FunctionState.cs index a3ecc1c..e2ff13e 100644 --- a/StateMachine/FunctionState.cs +++ b/StateMachine/FunctionState.cs @@ -5,33 +5,83 @@ /// public sealed class FunctionState : State { - private readonly FunctionStateFunctionDelegate? _enterFunc; - private readonly FunctionStateFunctionDelegate? _exitFunc; - private readonly FunctionStateFunctionDelegate _innerFunc; + private readonly FunctionStateFunctionDelegateWithVars? _enterFunc; + private readonly FunctionStateFunctionDelegateWithVars? _exitFunc; + private readonly FunctionStateFunctionDelegateWithVars _innerFunc; /// - /// Create an instance of with the passed . + /// Create an instance of with the passed . + /// + /// Name of the state + /// + public FunctionState(string name, FunctionStateFunctionDelegate innerFunc) : this(name, null, innerFunc, null) { } + + /// + /// Create an instance of with the passed . /// /// The function to call every cycle. - public FunctionState(FunctionStateFunctionDelegate innerFunc) - { - _innerFunc = innerFunc; - } + public FunctionState(FunctionStateFunctionDelegate innerFunc) : this("FunctionState", null, innerFunc, null) { } + + /// + /// Creates an instance of with name, entry, inner and exit . + /// + /// + /// Function to be called when entering the state, or null. + /// Function to be called every cycle. + /// function to be called when exiting the state, or null. + public FunctionState(string name, + FunctionStateFunctionDelegate? enterFunc, + FunctionStateFunctionDelegate innerFunc, + FunctionStateFunctionDelegate? exitFunc) : this(name, + enterFunc != null ? ((vars) => enterFunc()) : null, + (vars) => innerFunc(), + exitFunc != null ? ((vars) => exitFunc()) : null) + {} + /// - /// Creates an instance of with entry, inner and exit states + /// Creates an instance of with entry, inner and exit . /// /// Function to be called when entering the state, or null. /// Function to be called every cycle. + /// Function to be called when exiting the state, or null. + public FunctionState(FunctionStateFunctionDelegate? enterFunc, + FunctionStateFunctionDelegate innerFunc, + FunctionStateFunctionDelegate? exitFunc) : this("FunctionState", + enterFunc != null ? ((vars) => enterFunc()) : null, + (vars) => innerFunc(), + exitFunc != null ? ((vars) => exitFunc()) : null) { } + + /// + /// Create an instance of with the passed . + /// + /// Name of the state + /// + public FunctionState(string name, FunctionStateFunctionDelegateWithVars innerFunc) : this(name, null, innerFunc, null) { } + + /// + /// Create an instance of with the passed . + /// + /// The function to call every cycle. + public FunctionState(FunctionStateFunctionDelegateWithVars innerFunc) : this("FunctionState", null, innerFunc, null) { } + + /// + /// Creates an instance of with name, entry, inner and exit . + /// + /// + /// Function to be called when entering the state, or null. + /// Function to be called every cycle. /// function to be called when exiting the state, or null. - public FunctionState(FunctionStateFunctionDelegate? enterFunc, FunctionStateFunctionDelegate innerFunc, FunctionStateFunctionDelegate? exitFunc) + public FunctionState(string name, + FunctionStateFunctionDelegateWithVars? enterFunc, + FunctionStateFunctionDelegateWithVars innerFunc, + FunctionStateFunctionDelegateWithVars? exitFunc) : base(name) { _enterFunc = enterFunc; _exitFunc = exitFunc; _innerFunc = innerFunc; } - protected sealed override void Enter(IDictionary vars) { _enterFunc?.Invoke(vars); diff --git a/StateMachine/State.cs b/StateMachine/State.cs index 0a19262..959d389 100644 --- a/StateMachine/State.cs +++ b/StateMachine/State.cs @@ -8,6 +8,27 @@ public abstract class State : IDisposable private readonly List transitions = new(); private bool disposedValue; + public string? Name { get; } + + protected State(string? name = null) + { + Name = name; + } + + + /// + /// Adds a state this state can transition to + /// + /// state to transition to + /// condition to transition. Evaluated each time is run. If true, the state machine moves to the associated state. Use null to always transition. + /// Transition conditions are evaulated in the order they are added. + /// The state this method was called on + public State AddTransitionTo(State to, TransitionConditionDelegateWithVars condition) + { + transitions.Add(new Transition(to, condition)); + return this; + } + /// /// Adds a state this state can transition to /// @@ -15,12 +36,22 @@ public abstract class State : IDisposable /// condition to transition. Evaluated each time is run. If true, the state machine moves to the associated state. Use null to always transition. /// Transition conditions are evaulated in the order they are added. /// The state this method was called on - public State AddTransitionTo(State to, TransitionConditionDelegate? condition) + public State AddTransitionTo(State to, TransitionConditionDelegate condition) { transitions.Add(new Transition(to, condition)); return this; } + /// + /// Adds a state this state will always transition to + /// + /// state to always transition to + /// Transition conditions are evaulated in the order they are added. + public void AlwaysTransitionTo(State to) + { + transitions.Add(new Transition(to)); + } + internal State RunAndGetNextState(int delay, IDictionary vars) { Enter(vars); @@ -94,5 +125,10 @@ void IDisposable.Dispose() Dispose(disposing: true); GC.SuppressFinalize(this); } + + public override string ToString() + { + return Name ?? GetType().Name[..GetType().Name.LastIndexOf("State")]; + } } } diff --git a/StateMachine/StateEventArgs.cs b/StateMachine/StateEventArgs.cs new file mode 100644 index 0000000..87a90a1 --- /dev/null +++ b/StateMachine/StateEventArgs.cs @@ -0,0 +1,10 @@ +namespace RoadieRichStateMachine +{ + public delegate void StateEventHandler(object sender, StateEventArgs e); + public class StateEventArgs + { + State State { get; } + + public StateEventArgs(State state) { State = state; } + } +} \ No newline at end of file diff --git a/StateMachine/StateMachine.cs b/StateMachine/StateMachine.cs index a1e5950..76cea1d 100644 --- a/StateMachine/StateMachine.cs +++ b/StateMachine/StateMachine.cs @@ -12,8 +12,21 @@ public class StateMachine : IDisposable /// public State InitialState { get; set; } = ExitState; + /// + /// Time to pause between executions of + /// public int Delay { get; set; } = 0; + /// + /// Raised before a State is started + /// + public event StateEventHandler? StateStarting; + + /// + /// Raised when a State has finished + /// + public event StateEventHandler? StateFinished; + /// /// If a state's points to this state, the state machine is terminated. /// @@ -30,7 +43,10 @@ public void Run(IDictionary? vars = null) while (state != ExitState) { - state = state.RunAndGetNextState(Delay, myVars); + StateStarting?.Invoke(this, new StateEventArgs(state)); + State nextState = state.RunAndGetNextState(Delay, myVars); + StateFinished?.Invoke(this, new StateEventArgs(state)); + state = nextState; } } diff --git a/StateMachine/Transition.cs b/StateMachine/Transition.cs index 9571c24..e62a713 100644 --- a/StateMachine/Transition.cs +++ b/StateMachine/Transition.cs @@ -4,13 +4,21 @@ internal class Transition : IDisposable { private bool disposedValue; - internal Transition(State to, TransitionConditionDelegate? condition) + public Transition(State to) { To = to; + Condition = null; + } + + internal Transition(State to, TransitionConditionDelegate condition) : this(to, (vars) => condition()) { } + + internal Transition(State to, TransitionConditionDelegateWithVars condition) : this(to) + { Condition = condition; } + public State To { get; } - public TransitionConditionDelegate? Condition { get; } + public TransitionConditionDelegateWithVars? Condition { get; } public bool CheckCondition(IDictionary vars) { return Condition == null || Condition(vars); diff --git a/UnitTests/FunctionStateUnitTests.cs b/UnitTests/FunctionStateUnitTests.cs index 564ee1b..c98039c 100644 --- a/UnitTests/FunctionStateUnitTests.cs +++ b/UnitTests/FunctionStateUnitTests.cs @@ -19,9 +19,9 @@ public void FunctionStateRunsInnerFunctions() { var b = false; - var funcState = new FunctionState((vars) => b = true); + var funcState = new FunctionState(() => b = true); - funcState.AddTransitionTo(StateMachine.ExitState, null); + funcState.AlwaysTransitionTo(StateMachine.ExitState); using var sm = new StateMachine(); @@ -36,9 +36,9 @@ public void FunctionStateRunsEntryFunction() { var aBool = false; - var funcState = new FunctionState((vars) => aBool = true, (vars) => { }, null); + var funcState = new FunctionState(() => aBool = true, () => { }, null); - funcState.AddTransitionTo(StateMachine.ExitState, null); + funcState.AlwaysTransitionTo(StateMachine.ExitState); using var sm = new StateMachine(); @@ -53,9 +53,9 @@ public void FunctionStateRunsExitFunction() { var aBool = false; - var funcState = new FunctionState(null, (vars) => { }, (vars) => aBool = true); + var funcState = new FunctionState(null, () => { }, () => aBool = true); - funcState.AddTransitionTo(StateMachine.ExitState, null); + funcState.AlwaysTransitionTo(StateMachine.ExitState); using var sm = new StateMachine(); @@ -66,13 +66,13 @@ public void FunctionStateRunsExitFunction() } [TestMethod] - public void InnerIsOnlyCalledRepeatedly() + public void InnerIsCalledRepeatedly() { var anInt = 0; - var funcState = new FunctionState((vars) => anInt++); + var funcState = new FunctionState(() => anInt++); - funcState.AddTransitionTo(StateMachine.ExitState, (vars) => anInt > 10); + funcState.AddTransitionTo(StateMachine.ExitState, () => anInt > 10); using var sm = new StateMachine(); @@ -88,9 +88,9 @@ public void EnterIsOnlyCalledOnce() var anInt = 0; var anotherInt = 0; - var funcState = new FunctionState((vars) => anInt++, (vars) => anotherInt++, null); + var funcState = new FunctionState(() => anInt++, () => anotherInt++, null); - funcState.AddTransitionTo(StateMachine.ExitState, (vars) => anotherInt > 10); + funcState.AddTransitionTo(StateMachine.ExitState, () => anotherInt > 10); using var sm = new StateMachine(); @@ -107,9 +107,9 @@ public void ExitIsOnlyCalledOnce() var anInt = 0; var anotherInt = 0; - var funcState = new FunctionState(null, (vars) => anotherInt++, (vars) => anInt++); + var funcState = new FunctionState(null, () => anotherInt++, () => anInt++); - funcState.AddTransitionTo(StateMachine.ExitState, (vars) => anotherInt > 10); + funcState.AddTransitionTo(StateMachine.ExitState, () => anotherInt > 10); using var sm = new StateMachine(); diff --git a/UnitTests/StateMachineUnitTests.cs b/UnitTests/StateMachineUnitTests.cs index 49a00da..6bc490d 100644 --- a/UnitTests/StateMachineUnitTests.cs +++ b/UnitTests/StateMachineUnitTests.cs @@ -33,8 +33,8 @@ public void VarsCanBeModified() ["x"] = 0 }; - var funcState = new FunctionState((vars) => vars["x"]++); - funcState.AddTransitionTo(StateMachine.ExitState, null); + var funcState = new FunctionState("func state", (vars) => vars["x"]++); + funcState.AlwaysTransitionTo(StateMachine.ExitState); using var sm = new StateMachine(); sm.InitialState = funcState; @@ -42,5 +42,33 @@ public void VarsCanBeModified() Assert.AreEqual(expected: 1, actual: vars["x"]); } + + [TestMethod] + public void StartStateEventIsTriggered() + { + var state = new FunctionState("", (vars) => { }); + state.AlwaysTransitionTo(StateMachine.ExitState); + + using var sm = new StateMachine() { InitialState = state }; + int anInt = 0; + sm.StateStarting += (sender, e) => anInt++; + sm.Run(); + + Assert.AreEqual(expected: 1, actual: anInt); + } + + [TestMethod] + public void EndStateEventIsTriggered() + { + var state = new FunctionState("", (vars) => { }); + state.AlwaysTransitionTo(StateMachine.ExitState); + + using var sm = new StateMachine() { InitialState = state }; + int anInt = 0; + sm.StateFinished += (sender, e) => anInt++; + sm.Run(); + + Assert.AreEqual(expected: 1, actual: anInt); + } } } \ No newline at end of file diff --git a/UnitTests/StateUnitTests.cs b/UnitTests/StateUnitTests.cs new file mode 100644 index 0000000..d35ce0b --- /dev/null +++ b/UnitTests/StateUnitTests.cs @@ -0,0 +1,48 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using RoadieRichStateMachine; +namespace UnitTests +{ + [TestClass] + public class StateUnitTests + { + [TestMethod] + public void NameCanBePassedInConstrutor() + { + var state = new MyState("myState"); + Assert.AreEqual(expected: "myState", state.ToString()); + } + + [TestMethod] + public void NameCanBeInferred() + { + var state = new MyState(); + Assert.IsNotNull(state.ToString()); + } + + [TestMethod] + public void InferredNameDoesNotIncludeState() + { + var state = new MyState(); + Assert.AreEqual(expected: "My", state.ToString()); + } + + internal class MyState : State + { + public MyState(string? name = null) : base(name) + { + } + + protected override void Enter(IDictionary vars) + { + } + + protected override void Exit(IDictionary vars) + { + } + + protected override void Inner(IDictionary vars) + { + } + } + } +} \ No newline at end of file