Maintaining State in Mud Engine
- by Johnathon Sullinger
I am currently working on a Mud Engine and have started implementing my state engine. One of the things that has me troubled is maintaining different states at once. For instance, lets say that the user has started a tutorial, which requires specific input. If the user types "help" I want to switch in to a help state, so they can get the help they need, then return them to the original state once exiting the help.
my state system uses a State Manager to manage the state per user:
public class StateManager
{
/// <summary>
/// Gets the current state.
/// </summary>
public IState CurrentState { get; private set; }
/// <summary>
/// Gets the states available for use.
/// </summary>
/// <value>
public List<IState> States { get; private set; }
/// <summary>
/// Gets the commands available.
/// </summary>
public List<ICommand> Commands { get; private set; }
/// <summary>
/// Gets the mob that this manager controls the state of.
/// </summary>
public IMob Mob { get; private set; }
public void Initialize(IMob mob, IState initialState = null)
{
this.Mob = mob;
if (initialState != null)
{
this.SwitchState(initialState);
}
}
/// <summary>
/// Performs the command.
/// </summary>
/// <param name="message">The message.</param>
public void PerformCommand(IMessage message)
{
if (this.CurrentState != null)
{
ICommand command = this.CurrentState.GetCommand(message);
if (command is NoOpCommand)
{
// NoOperation commands indicate that the current state is not finished yet.
this.CurrentState.Render(this.Mob);
}
else if (command != null)
{
command.Execute(this.Mob);
}
else if (command == null)
{
new InvalidCommand().Execute(this.Mob);
}
}
}
/// <summary>
/// Switches the state.
/// </summary>
/// <param name="state">The state.</param>
public void SwitchState(IState state)
{
if (this.CurrentState != null)
{
this.CurrentState.Cleanup();
}
this.CurrentState = state;
if (state != null)
{
this.CurrentState.Render(this.Mob);
}
}
}
Each of the different states that the user can be in, is a Type implementing IState.
public interface IState
{
/// <summary>
/// Renders the current state to the players terminal.
/// </summary>
/// <param name="player">The player to render to</param>
void Render(IMob mob);
/// <summary>
/// Gets the Command that the player entered and preps it for execution.
/// </summary>
/// <returns></returns>
ICommand GetCommand(IMessage command);
/// <summary>
/// Cleanups this instance during a state change.
/// </summary>
void Cleanup();
}
Example state:
public class ConnectState : IState
{
/// <summary>
/// The connected player
/// </summary>
private IMob connectedPlayer;
public void Render(IMob mob)
{
if (!(mob is IPlayer))
{
throw new NullReferenceException("ConnectState can only be used with a player object implementing IPlayer");
}
//Store a reference for the GetCommand() method to use.
this.connectedPlayer = mob as IPlayer;
var server = mob.Game as IServer;
var game = mob.Game as IGame;
// It is not guaranteed that mob.Game will implement IServer. We are only guaranteed that it will implement IGame.
if (server == null)
{
throw new NullReferenceException("LoginState can only be set to a player object that is part of a server.");
}
//Output the game information
mob.Send(new InformationalMessage(game.Name));
mob.Send(new InformationalMessage(game.Description));
mob.Send(new InformationalMessage(string.Empty)); //blank line
//Output the server MOTD information
mob.Send(new InformationalMessage(string.Join("\n", server.MessageOfTheDay)));
mob.Send(new InformationalMessage(string.Empty)); //blank line
mob.StateManager.SwitchState(new LoginState());
}
/// <summary>
/// Gets the command.
/// </summary>
/// <param name="message">The message.</param>
/// <returns>Returns no operation required.</returns>
public Commands.ICommand GetCommand(IMessage message)
{
return new NoOpCommand();
}
/// <summary>
/// Cleanups this instance during a state change.
/// </summary>
public void Cleanup()
{
// We have nothing to clean up.
return;
}
}
With the way that I have my FSM set up at the moment, the user can only ever have one state at a time. I read a few different posts on here about state management but nothing regarding keeping a stack history.
I thought about using a Stack collection, and just pushing new states on to the stack then popping them off as the user moves out from one. It seems like it would work, but I'm not sure if it is the best approach to take.
I'm looking for recommendations on this. I'm currently swapping state from within the individual states themselves as well which I'm on the fence about if it makes sense to do there or not. The user enters a command, the StateManager passes the command to the current State and lets it determine if it needs it (like passing in a password after entering a user name), if the state doesn't need any further commands, it returns null. If it does need to continue doing work, it returns a No Operation to let the state manager know that the state still requires further input from the user. If null is returned, the state manager will then go find the appropriate state for the command entered by the user.
Example state requiring additional input from the user
public class LoginState : IState
{
/// <summary>
/// The connected player
/// </summary>
private IPlayer connectedPlayer;
private enum CurrentState
{
FetchUserName,
FetchPassword,
InvalidUser,
}
private CurrentState currentState;
/// <summary>
/// Renders the current state to the players terminal.
/// </summary>
/// <param name="mob"></param>
/// <exception cref="System.NullReferenceException">
/// ConnectState can only be used with a player object implementing IPlayer
/// or
/// LoginState can only be set to a player object that is part of a server.
/// </exception>
public void Render(IMob mob)
{
if (!(mob is IPlayer))
{
throw new NullReferenceException("ConnectState can only be used with a player object implementing IPlayer");
}
//Store a reference for the GetCommand() method to use.
this.connectedPlayer = mob as IPlayer;
var server = mob.Game as IServer;
// Register to receive new input from the user.
mob.ReceivedMessage += connectedPlayer_ReceivedMessage;
if (server == null)
{
throw new NullReferenceException("LoginState can only be set to a player object that is part of a server.");
}
this.currentState = CurrentState.FetchUserName;
switch (this.currentState)
{
case CurrentState.FetchUserName:
mob.Send(new InputMessage("Please enter your user name"));
break;
case CurrentState.FetchPassword:
mob.Send(new InputMessage("Please enter your password"));
break;
case CurrentState.InvalidUser:
mob.Send(new InformationalMessage("Invalid username/password specified."));
this.currentState = CurrentState.FetchUserName;
mob.Send(new InputMessage("Please enter your user name"));
break;
}
}
/// <summary>
/// Receives the players input.
/// </summary>
/// <param name="sender">The sender.</param>
/// <param name="e">The e.</param>
void connectedPlayer_ReceivedMessage(object sender, IMessage e)
{
// Be good memory citizens and clean ourself up after receiving a message.
// Not doing this results in duplicate events being registered and memory leaks.
this.connectedPlayer.ReceivedMessage -= connectedPlayer_ReceivedMessage;
ICommand command = this.GetCommand(e);
}
/// <summary>
/// Gets the Command that the player entered and preps it for execution.
/// </summary>
/// <param name="command"></param>
/// <returns>Returns the ICommand specified.</returns>
public Commands.ICommand GetCommand(IMessage command)
{
if (this.currentState == CurrentState.FetchUserName)
{
this.connectedPlayer.Name = command.Message;
this.currentState = CurrentState.FetchPassword;
}
else if (this.currentState == CurrentState.FetchPassword)
{
// find user
}
return new NoOpCommand();
}
/// <summary>
/// Cleanups this instance during a state change.
/// </summary>
public void Cleanup()
{
// If we have a player instance, we clean up the registered event.
if (this.connectedPlayer != null)
{
this.connectedPlayer.ReceivedMessage -= this.connectedPlayer_ReceivedMessage;
}
}
Maybe my entire FSM isn't wired up in the best way, but I would appreciate input on what would be the best to maintain a stack of state in a MUD game engine, and if my states should be allowed to receive the input from the user or not to check what command was entered before allowing the state manager to switch states.
Thanks in advance.