# Logon state management

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?  |
|            Y          N-->-
Correct?        |
N     Y            |              |
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
{
ConnectedState _state = ConnectedState.ExpectingPlayerName;
IPlayerConnection _playerConnection;
private Player _player;
private string _playerName;

public LogonSession(IPlayerConnection playerConnection, IMudConfiguration configuration, IPlayerRepository playerRepository, ISessionManager sessionManager)
{
_playerConnection = playerConnection;
_playerRepository = playerRepository;
_configuration = configuration;
_sessionManager = sessionManager;

_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")
{
}
else
{
_playerConnection.SendText("\r\nWhat is it then? ");
_state = ConnectedState.ExpectingPlayerName;
}
break;

break;

_state = ConnectedState.LoggedOn;
break;

{
_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 Player _player;
private IPlayerConnection _playerConnection;

public PlayerSession(IPlayerConnection playerConnection, Player player, IMudControl mudController, IMudConfiguration configuration)
{
_playerConnection = playerConnection;
_player = player;
_mudController = mudController;
_configuration = configuration;

_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 enums 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;

ISessionInputHandler _sessionInputHandler;

public PlayerSession(IPlayerConnection playerConnection, IMudConfiguration configuration, IPlayerRepository playerRepository, ISessionManager sessionManager, IMudControl mudController)
{
_playerConnection = playerConnection;
_playerRepository = playerRepository;
_configuration = configuration;
_sessionManager = sessionManager;

_sessionInputHandlerFactory = new SessionInputHandlerFactory(this, configuration, playerRepository, mudController);
}

{
if (null != _sessionInputHandler)
{
}
}

public void ActivateInputHandler(ISessionInputHandler inputHandler)
{
_sessionInputHandler = inputHandler;

_sessionInputHandler.ActivateHandler(_playerConnection);
}
}


ISessionInputHandler

public interface ISessionInputHandler
{
void ActivateHandler(IPlayerConnection playerConnection);
}


SessionInputHandlerFactory

Creates the various classes for handling different states.

class SessionInputHandlerFactory
{

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 ActivateEndSession()
{
_session.ActivateInputHandler(new EndSessionHandler(this));
}

internal void ActivateExpectingPlayerName(bool retrying)
{
_session.ActivateInputHandler(new LogonExpectingPlayerNameHandler(_playerRepository, retrying, 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
{

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")
{
}
else
{
_sessionInputHandlerFactory.ActivateExpectingPlayerName(true);
}
}
}


internal class LogonGettingNewPlayerPasswordHandler : ISessionInputHandler
{

{
this.playerName = playerName;
this.sessionInputHandlerFactory = sessionInputHandlerFactory;
}

public void ActivateHandler(IPlayerConnection playerConnection)
{
}

{
}
}


internal class LogonConfirmingNewPlayerPasswordHandler : ISessionInputHandler
{

{
_playerName = playerName;
_sessionInputHandlerFactory = sessionInputHandlerFactory;
_playerRepository = playerRepository;
}

public void ActivateHandler(IPlayerConnection playerConnection)
{
}

{
{
return;
}

_sessionInputHandlerFactory.ActivateLoggedOn(player);
}
}


LoggedOnInputHandler

internal class LoggedOnInputHandler : ISessionInputHandler
{

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.

• Approach 2 is the way to go. It allows for seperation of control and the highest amount in flexibility of flow changes. – dfhwze May 21 at 17:39

Scratching the tip of the Approach 2 iceberg...

public void ReceivedLine(string text)
{
if (null != _sessionInputHandler)
{
}
}


The name of the method suggests it should be a property but it looks like an event. Did you mean OnLineReceived? If it's a method for raising events then it shouldn't be public.

null != _sessionInputHandler


I've never seen someone writing conditions backwards ;-)

SessionInputHandlerFactory


The name suggests it's a factory but it doesn't really create anything. It has a lot of ActivateX methods. It's not what I would expect from a factory. It should rather be some activator or configurator.

var player = _playerRepository.CreatePlayer(_playerName, _password);


Repositories don't create things. They store them. For creating things we use factories.

Player


You have an abstraction for almost everything but the Player. In the LoggedOnInputHandler you use only its name. I don't think it's necessary to pass the entire object and make the handler depend on it.

# Approach 3

I find both approaches are really complex, perhaps even too complex. I'd like to suggest yet another one. This should resemble the MVVM design.

Start by creating views. I call them Screens where each screen is derived from the Screen type.

abstract class Screen
{
public Screen NextScreen { get; protected set; }

public abstract void Show();

protected void WriteLine(string message)
{
Console.WriteLine(message);
}

{
}
}

class WelcomeScreen : Screen
{
public override void Show()
{
WriteLine("Welcome to Mud.");
WriteLine("Press Enter to logon or Escape to exit.");

do
{
if (key == ConsoleKey.Enter)
{
NextScreen = new LogonScreen();
return;
}
else if (key == ConsoleKey.Escape)
{
NextScreen = null;
return;
}
}
while(true);
}
}

class LogonScreen : Screen
{
public override void Show()
{
}
}

class NewUserScreen : Screen
{
public override void Show()
{

}
}


In the next step you can create a data model for each screen. Let's say LogonScreenModel. This will handle the logon process against e.g. a database.

You can start the game with the Game object:

class Game
{
public Game()
{
// initilize something here if necessary
}

public void Start()
{
var welcomeScreen = new WelcomeScreen();
welcomeScreen.Show();
if (welcomeScreen.NextScreen == null)
{
return;
}
welcomeScreen.NextScreen.Show();
}
}

• The null != is a hangover from C to minimise typo errors so whilst if(someVal = null) would error in C# it works in C. You're right about the Factory, it started off just creating the handlers rather than activating them and the name hasn't kept up. – forsvarir Nov 15 '16 at 15:47
• @forsvarir I've added approach 3 as a new suggestion. – t3chb0t Nov 16 '16 at 9:46
• Option 3 is an interesting approach, although I'll have to think about how I would implement it. Whilst IPlayerConnection is currently only a wrapper for the Console it will also wrap network connections and I'd rather avoid having a thread per connection which is why my starting point was input / state based. – forsvarir Nov 16 '16 at 11:02