5
\$\begingroup\$

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 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;
    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.

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

Scratching the tip of the Approach 2 iceberg...


public void ReceivedLine(string text)
{
  if (null != _sessionInputHandler)
  {
      _sessionInputHandler.ReceivedLine(_playerConnection, text);
  }
}

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);
    }

    protected ConsoleKeyInfo ReadKey()
    {
        return Console.ReadKey();
    }
}

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

        do 
        {
            var key = ReadKey().Key;
            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()
    {
        WriteLine("What is your name?");
        ReadKey();
    }
}

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();
    }
}
\$\endgroup\$
  • 2
    \$\begingroup\$ 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. \$\endgroup\$ – forsvarir Nov 15 '16 at 15:47
  • 1
    \$\begingroup\$ @forsvarir I've added approach 3 as a new suggestion. \$\endgroup\$ – t3chb0t Nov 16 '16 at 9:46
  • \$\begingroup\$ 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. \$\endgroup\$ – forsvarir Nov 16 '16 at 11:02

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.