I'm in the process of writing a simple text based MUD. First up is making sure that it's possible to create new players / logon with existing players. Since everything is text based, there are several state transitions involved where the user is prompted and then their response is processed. With this in mind, I've created some code for handling these states:
Get User Name <--------------
| |
Is User Known? |
| | |
Y N |
| | |
Get User Confirm Name? |
Password | | |
| Y N-->-
Correct? |
| | Get Password <-------
N Y | |
| | Confirm Password |
Close Logged On | |
Matches? |
| | |
Y N----->-
|
Logged On
Once logged on, interactions will take on a more fluid approach.
Approach 1
My first draft split the processing up between two session classes. One for managing the logon transitions (LogonSession
) and one for post logon (PlayerSession
):
LogonSession
public class LogonSession : ISession, IInputLineListener
{
enum ConnectedState { ExpectingPlayerName, ExpectingExistingPassword, Closed, ConfirmingPlayerName, GettingPlayerPassword, ConfirmingPassword, LoggedOn };
ConnectedState _state = ConnectedState.ExpectingPlayerName;
IPlayerConnection _playerConnection;
private readonly IMudConfiguration _configuration;
private readonly IPlayerRepository _playerRepository;
private Player _player;
private string _playerName;
private string _password;
private readonly ISessionManager _sessionManager;
public LogonSession(IPlayerConnection playerConnection, IMudConfiguration configuration, IPlayerRepository playerRepository, ISessionManager sessionManager)
{
_playerConnection = playerConnection;
_playerRepository = playerRepository;
_configuration = configuration;
_sessionManager = sessionManager;
_playerConnection.SetReceivedLineListener(this);
_playerConnection.SendText($"Welcome to {_configuration.Name} Mud.\r\n");
_playerConnection.SendText("What is your name?");
}
public void ReceivedLine(string text)
{
switch (_state)
{
case ConnectedState.ExpectingPlayerName:
if (_playerRepository.PlayerExists(text))
{
_player = _playerRepository.GetPlayer(text);
_playerConnection.SendText("Password:");
_state = ConnectedState.ExpectingExistingPassword;
}
else
{
_playerName = text;
_playerConnection.SendText($"Did I get it right, {_playerName} (Y/N)?");
_state = ConnectedState.ConfirmingPlayerName;
}
break;
case ConnectedState.ConfirmingPlayerName:
if (text.ToUpper() == "Y")
{
_playerConnection.SendText("Please enter a password:");
_state = ConnectedState.GettingPlayerPassword;
}
else
{
_playerConnection.SendText("\r\nWhat is it then? ");
_state = ConnectedState.ExpectingPlayerName;
}
break;
case ConnectedState.GettingPlayerPassword:
_password = text;
_playerConnection.SendText("Confirm your password:");
_state = ConnectedState.ConfirmingPassword;
break;
case ConnectedState.ConfirmingPassword:
_player = _playerRepository.CreatePlayer(_playerName, _password);
_state = ConnectedState.LoggedOn;
break;
case ConnectedState.ExpectingExistingPassword:
if (_player.Password != text)
{
_playerConnection.SendText("Sorry, I didn't recognise your password. Are you sure you're who you think you are?\r\n");
_playerConnection.SendText("Goodbye.\n");
_state = ConnectedState.Closed;
}
else
{
_state = ConnectedState.LoggedOn;
}
break;
}
if(_state == ConnectedState.LoggedOn)
{
_sessionManager.CreatePlayerSession(_playerConnection, _player);
}
}
}
PlayerSession
class PlayerSession : ISession, IInputLineListener
{
private readonly IMudConfiguration _configuration;
private readonly IMudControl _mudController;
private Player _player;
private IPlayerConnection _playerConnection;
public PlayerSession(IPlayerConnection playerConnection, Player player, IMudControl mudController, IMudConfiguration configuration)
{
_playerConnection = playerConnection;
_player = player;
_mudController = mudController;
_configuration = configuration;
_playerConnection.SetReceivedLineListener(this);
_playerConnection.SendText($"Welcome to {_configuration.Name} Mud {_player.Name}, enjoy your visit.\r\n");
}
public void ReceivedLine(string text)
{
switch(text)
{
case "shutdown":
_playerConnection.SendText("Shutting down!\r\n");
_playerConnection.SendText("Goodbye.\r\n");
_playerConnection.Close(true);
_mudController.Shutdown();
break;
case "quit":
_playerConnection.SendText("Goodbye.\r\n");
_playerConnection.Close();
return;
default:
_playerConnection.SendText($"Sorry, I don't understand: {text}\r\n");
break;
}
}
}
Approach 2
I know I'm going to need to add some more states in the future (for character creation) and the LogonSession
from approach 1 already felt like it was getting a bit busy handling all the state transitions in the switch method. I'm also not super keen on using enum
s for states that have branches (where a higher value doesn't necessarily mean you're further along the process). So I've refactored the code to user state handlers. This means that I don't have a LogonSession
anymore, I just have a PlayerSession
and multiple state handlers.
PlayerSession
public class PlayerSession : ISession, IInputLineListener
{
IPlayerConnection _playerConnection;
private readonly IMudConfiguration _configuration;
private readonly IPlayerRepository _playerRepository;
private readonly ISessionManager _sessionManager;
ISessionInputHandler _sessionInputHandler;
private readonly SessionInputHandlerFactory _sessionInputHandlerFactory;
public PlayerSession(IPlayerConnection playerConnection, IMudConfiguration configuration, IPlayerRepository playerRepository, ISessionManager sessionManager, IMudControl mudController)
{
_playerConnection = playerConnection;
_playerRepository = playerRepository;
_configuration = configuration;
_sessionManager = sessionManager;
_playerConnection.SetReceivedLineListener(this);
_sessionInputHandlerFactory = new SessionInputHandlerFactory(this, configuration, playerRepository, mudController);
}
public void ReceivedLine(string text)
{
if (null != _sessionInputHandler)
{
_sessionInputHandler.ReceivedLine(_playerConnection, text);
}
}
public void ActivateInputHandler(ISessionInputHandler inputHandler)
{
_sessionInputHandler = inputHandler;
_sessionInputHandler.ActivateHandler(_playerConnection);
}
}
ISessionInputHandler
public interface ISessionInputHandler
{
void ActivateHandler(IPlayerConnection playerConnection);
void ReceivedLine(IPlayerConnection playerConnection, string text);
}
SessionInputHandlerFactory
Creates the various classes for handling different states.
class SessionInputHandlerFactory
{
private readonly IMudConfiguration _configuration;
private readonly ISession _session;
private readonly IPlayerRepository _playerRepository;
private readonly IMudControl _mudController;
public SessionInputHandlerFactory(ISession session, IMudConfiguration configuration, IPlayerRepository playerRepository, IMudControl mudController)
{
_session = session;
_configuration = configuration;
_playerRepository = playerRepository;
_mudController = mudController;
ActivateNewConnectionHandler();
}
internal void ActivateNewConnectionHandler()
{
_session.ActivateInputHandler(new NewConnectionHandler(_configuration, this));
}
internal void ActivateExpectingExistingPlayerPassword(Player player)
{
_session.ActivateInputHandler(new LogonExpectingExistingPlayerPasswordHandler(player, this));
}
internal void ActivateEndSession()
{
_session.ActivateInputHandler(new EndSessionHandler(this));
}
internal void ActivateExpectingPlayerName(bool retrying)
{
_session.ActivateInputHandler(new LogonExpectingPlayerNameHandler(_playerRepository, retrying, this));
}
internal void ActivateConfirmingNewPlayerPassword(string playerName, string password)
{
_session.ActivateInputHandler(new LogonConfirmingNewPlayerPasswordHandler(playerName, password, _playerRepository, this));
}
internal void ActivateGettingNewPlayerPassword(string playerName)
{
_session.ActivateInputHandler(new LogonGettingNewPlayerPasswordHandler(playerName, this));
}
internal void ActivateConfirmingNewPlayerName(string playerName)
{
_session.ActivateInputHandler(new LogonConfirmingNewPlayerNameHandler(playerName, this));
}
internal void ActivateLoggedOn(Player player)
{
_session.ActivateInputHandler(new LoggedOnInputHandler(player, _configuration, _mudController, this));
}
}
NewConnectionHandler
class NewConnectionHandler : ISessionInputHandler
{
private readonly SessionInputHandlerFactory _sessionInputHandlerFactory;
private readonly IMudConfiguration _configuration;
public NewConnectionHandler(IMudConfiguration configuration, SessionInputHandlerFactory sessionInputHandlerFactory)
{
_configuration = configuration;
_sessionInputHandlerFactory = sessionInputHandlerFactory;
}
public void ActivateHandler(IPlayerConnection playerConnection)
{
playerConnection.SendText($"Welcome to {_configuration.Name} Mud.\r\n");
_sessionInputHandlerFactory.ActivateExpectingPlayerName(false);
}
public void ReceivedLine(IPlayerConnection playerConnection, string text)
{
}
}
LogonExpectingPlayerNameHandler
class LogonExpectingPlayerNameHandler : ISessionInputHandler
{
private readonly SessionInputHandlerFactory _handlerFactory;
private readonly IPlayerRepository _playerRepository;
private readonly bool _retrying;
public LogonExpectingPlayerNameHandler(IPlayerRepository playerRepository, bool retrying, SessionInputHandlerFactory handlerFactory)
{
_playerRepository = playerRepository;
_handlerFactory = handlerFactory;
_retrying = retrying;
}
public void ActivateHandler(IPlayerConnection playerConnection)
{
if(_retrying)
{
playerConnection.SendText("What is it then? ");
}
else
{
playerConnection.SendText("What is your name?");
}
}
public void ReceivedLine(IPlayerConnection playerConnection, string playerName)
{
if (_playerRepository.PlayerExists(playerName))
{
_handlerFactory.ActivateExpectingExistingPlayerPassword(_playerRepository.GetPlayer(playerName));
}
else
{
_handlerFactory.ActivateConfirmingNewPlayerName(playerName);
}
}
}
LogonExpectingExistingPlayerPasswordHandler
internal class LogonExpectingExistingPlayerPasswordHandler : ISessionInputHandler
{
private readonly Player _player;
private readonly SessionInputHandlerFactory _sessionInputHandlerFactory;
public LogonExpectingExistingPlayerPasswordHandler(Player player, SessionInputHandlerFactory sessionInputHandlerFactory)
{
_player = player;
_sessionInputHandlerFactory = sessionInputHandlerFactory;
}
public void ActivateHandler(IPlayerConnection playerConnection)
{
playerConnection.SendText("Password:");
}
public void ReceivedLine(IPlayerConnection playerConnection, string text)
{
if (_player.Password != text)
{
playerConnection.SendText("Sorry, I didn't recognise your password. Are you sure you're who you think you are?\r\n");
_sessionInputHandlerFactory.ActivateEndSession();
}
else
{
_sessionInputHandlerFactory.ActivateLoggedOn(_player);
}
}
}
EndSessionHandler
internal class EndSessionHandler : ISessionInputHandler
{
private SessionInputHandlerFactory sessionInputHandlerFactory;
public EndSessionHandler(SessionInputHandlerFactory sessionInputHandlerFactory)
{
this.sessionInputHandlerFactory = sessionInputHandlerFactory;
}
public void ActivateHandler(IPlayerConnection playerConnection)
{
playerConnection.SendText("Goodbye.\r\n\r\n");
playerConnection.Close();
}
public void ReceivedLine(IPlayerConnection playerConnection, string text)
{
}
}
LogonConfirmingNewPlayerNameHandler
internal class LogonConfirmingNewPlayerNameHandler : ISessionInputHandler
{
private readonly string _playerName;
private readonly SessionInputHandlerFactory _sessionInputHandlerFactory;
public LogonConfirmingNewPlayerNameHandler(string playerName, SessionInputHandlerFactory sessionInputHandlerFactory)
{
_playerName = playerName;
_sessionInputHandlerFactory = sessionInputHandlerFactory;
}
public void ActivateHandler(IPlayerConnection playerConnection)
{
playerConnection.SendText($"Did I get it right, {_playerName} (Y/N)?");
}
public void ReceivedLine(IPlayerConnection playerConnection, string confirmation)
{
if (confirmation.ToUpper() == "Y")
{
_sessionInputHandlerFactory.ActivateGettingNewPlayerPassword(_playerName);
}
else
{
_sessionInputHandlerFactory.ActivateExpectingPlayerName(true);
}
}
}
LogonGettingNewPlayerPasswordHandler
internal class LogonGettingNewPlayerPasswordHandler : ISessionInputHandler
{
private readonly string playerName;
private readonly SessionInputHandlerFactory sessionInputHandlerFactory;
public LogonGettingNewPlayerPasswordHandler(string playerName, SessionInputHandlerFactory sessionInputHandlerFactory)
{
this.playerName = playerName;
this.sessionInputHandlerFactory = sessionInputHandlerFactory;
}
public void ActivateHandler(IPlayerConnection playerConnection)
{
playerConnection.SendText("Please enter a password:");
}
public void ReceivedLine(IPlayerConnection playerConnection, string password)
{
sessionInputHandlerFactory.ActivateConfirmingNewPlayerPassword(playerName, password);
}
}
LogonConfirmingNewPlayerPasswordHandler
internal class LogonConfirmingNewPlayerPasswordHandler : ISessionInputHandler
{
private readonly string _password;
private readonly string _playerName;
private readonly IPlayerRepository _playerRepository;
private readonly SessionInputHandlerFactory _sessionInputHandlerFactory;
public LogonConfirmingNewPlayerPasswordHandler(string playerName, string password, IPlayerRepository playerRepository, SessionInputHandlerFactory sessionInputHandlerFactory)
{
_playerName = playerName;
_password = password;
_sessionInputHandlerFactory = sessionInputHandlerFactory;
_playerRepository = playerRepository;
}
public void ActivateHandler(IPlayerConnection playerConnection)
{
playerConnection.SendText("Confirm your password:");
}
public void ReceivedLine(IPlayerConnection playerConnection, string passwordConfirmation)
{
if(_password != passwordConfirmation)
{
playerConnection.SendText("Password doesn't match, please try again\r\n");
_sessionInputHandlerFactory.ActivateGettingNewPlayerPassword(_playerName);
return;
}
var player = _playerRepository.CreatePlayer(_playerName, _password);
_sessionInputHandlerFactory.ActivateLoggedOn(player);
}
}
LoggedOnInputHandler
internal class LoggedOnInputHandler : ISessionInputHandler
{
private readonly IMudConfiguration _configuration;
private readonly IMudControl _mudController;
private readonly Player _player;
private readonly SessionInputHandlerFactory _sessionInputHandlerFactory;
public LoggedOnInputHandler(Player player, IMudConfiguration configuration, IMudControl mudController, SessionInputHandlerFactory sessionInputHandlerFactory)
{
_player = player;
_sessionInputHandlerFactory = sessionInputHandlerFactory;
_configuration = configuration;
_mudController = mudController;
}
public void ActivateHandler(IPlayerConnection playerConnection)
{
playerConnection.SendText($"Welcome to {_configuration.Name} Mud {_player.Name}, enjoy your visit.\r\n");
}
public void ReceivedLine(IPlayerConnection playerConnection, string text)
{
switch (text)
{
case "shutdown":
playerConnection.SendText("Shutting down!\r\n");
_mudController.Shutdown();
_sessionInputHandlerFactory.ActivateEndSession();
break;
case "quit":
_sessionInputHandlerFactory.ActivateEndSession();
return;
default:
playerConnection.SendText($"Sorry, I don't understand: {text}\r\n");
break;
}
}
}
In general, I think I prefer the second approach, because whilst there is more code overall, each handler has a distinct job in the flow. I can see how I can expand it with more states to handle player creation / add better validation to each step (for example adding retries on failed password entry).
That said, there are somethings I am not so keen on. At the moment, the handlers all implement ISessionInputHandler
which makes sense for most of them, however some (such as the EndSessionHandler
and NewConnectionHandler
) only transmit to the client, they don't process input. Does this mean the name's wrong or am I shoehorning handlers in that shouldn't really be there?
Any and all feedback welcome. This is a work in progress, so some of the periphery classes (repository etc are stubbed) and others like the LoggedOnInputHandler
will change as more commands are implemented. I think I've added all the relevant code for the review, however I'll add more if needed or it's available here.