C# PROGRAMMING

Raymond1985
InitialcodesofLab8.docx

Lab week 8 – Implementing the Command Pattern and NPCs

Codebase

This lab continues from last week. You need to have completed lab 7 to continue with this labsheet and you are expected to use your own code from last week to work on this week’s tasks.

Assignment 2 Hand up

You need to submit the following from today’s lab sheet for assignment 2:

· Completed codebase zipped up as lab8.

·

Implementing the Command Pattern

The DungeonMaster class is already looking very messy (and is only going to get worse as we continue to add commands to the game). If we consider the current sequence diagram for processing a turn:

We can see that already I have had to simplify the processCommand logic (actually what is really going on is more complicated), and we have already started to break things up with dedicated methods within DungeonMaster to help deal with the logic (like processMove). This is only going to get worse over time.

One way we can clean this up is to introduce dedicated classes to handle each command, lucky for us this problem has been tackled before and there is a dedicated command pattern which comes to our rescue!

Let’s start by specifying an abstract class to represent commands in general. Create a new class called Command in the Control package as follows:

using Mazegame.Entity;

namespace Mazegame.Control

{

public abstract class Command

{

public abstract CommandResponse Execute(ParsedInput userInput,

Player thePlayer);

}

}

Each command will take the parsed input and a reference to the player and return some kind of response. In our case looking at the first 2 commands we have implemented (quit and move) the response is either some kind of flag as to whether we need to quit the game, or some kind of message to display to the user. So let’s specify our CommandResponse class:

namespace Mazegame.Control

{

public class CommandResponse

{

private bool finishedGame;

private string message;

public CommandResponse(string message)

{

Message = message;

FinishedGame = false;

}

public CommandResponse(string message, bool quitFlag)

{

Message = message;

FinishedGame = quitFlag;

}

public bool FinishedGame

{

get { return finishedGame; }

set { finishedGame = value; }

}

public string Message

{

get { return message; }

set { message = value; }

}

}

}

We are now in a position to write a command class for move and another for quit.

namespace Mazegame.Control

{

public class QuitCommand : Command

{

public override CommandResponse Execute(ParsedInput userInput, Player thePlayer)

{

return new CommandResponse("Thanks for playing -- Goodbye",true);

}

}

}

That’s it for Quit (about the easiest command you could imagine!). Now let’s create a class for Move:

namespace Mazegame.Control

{

public class MoveCommand : Command

{

public override CommandResponse Execute(ParsedInput userInput, Player thePlayer)

{

String exitLabel = (String) userInput.Arguments[0];

Exit desiredExit = thePlayer.CurrentLocation.GetExit(exitLabel);

if (desiredExit == null)

{

return new CommandResponse("There is no exit there.. Trying moving someplace moveable!!");

}

thePlayer.CurrentLocation = desiredExit.Destination;

return new CommandResponse("You find yourself looking at " + thePlayer.CurrentLocation.Description);

}

}

}

So now we have defined our command classes, but we haven’t implemented them yet.

Before we change DungeonMaster let’s introduce one more class to keep track of the available commands. Create a new Control class called CommandHandler as follows:

namespace Mazegame.Control

{

public class CommandHandler

{

private Hashtable availableCommands;

private Parser theParser;

public CommandHandler()

{

availableCommands = new Hashtable();

SetupCommands();

theParser = new Parser(new ArrayList(availableCommands.Keys));

}

private void SetupCommands()

{

availableCommands.Add("go", new MoveCommand());

availableCommands.Add("quit", new QuitCommand());

availableCommands.Add("move", new MoveCommand());

}

public CommandResponse ProcessTurn(String userInput, Player thePlayer)

{

ParsedInput validInput = theParser.Parse(userInput);

try

{

Command theCommand = (Command) availableCommands[validInput.Command];

return theCommand.Execute(validInput, thePlayer);

}

catch (KeyNotFoundException)

{

return new CommandResponse("Not a valid command");

}

}

}

}

So here we maintain a Hashtable for each command class, indexed by a label. The beauty of this is:

1. We can set up aliases for different commands (such as move and go pointing to the same command).

2. When we develop new command classes we simple define the class and add it to the hashtable with a label and it works.

OK now we have our CommandHandler it is time to change DungeonMaster:

namespace Mazegame.Control

{

public class DungeonMaster

{

private IMazeClient gameClient;

private IMazeData gameData;

private Player thePlayer;

private CommandHandler playerTurnHandler;

public DungeonMaster(IMazeData gameData, IMazeClient gameClient)

{

this.gameData = gameData;

this.gameClient = gameClient;

playerTurnHandler = new CommandHandler();

}

public void PrintWelcome()

{

gameClient.PlayerMessage(gameData.GetWelcomeMessage());

}

public void SetupPlayer()

{

String playerName = gameClient.GetReply("What name do you choose to be known by?");

thePlayer = new Player(playerName);

thePlayer.CurrentLocation = gameData.GetStartingLocation();

gameClient.PlayerMessage("Welcome " + playerName + "\n\n");

gameClient.PlayerMessage("You find yourself looking at ");

gameClient.PlayerMessage(thePlayer.CurrentLocation.Description);

}

public void RunGame()

{

PrintWelcome();

SetupPlayer();

while (handlePlayerTurn())

{

// handle npc logic later here

}

gameClient.GetReply("\n\n<<Hit enter to exit>>");

}

private bool handlePlayerTurn()

{

CommandResponse playerResponse = playerTurnHandler.ProcessTurn(gameClient.GetCommand(), thePlayer);

gameClient.PlayerMessage(playerResponse.Message);

return !playerResponse.FinishedGame;

}

} //end DungeonMaster

} //end namespace Control

Our code is now cleaner and we have a flexible, extensible structure to continue adding commands. The revised process player turn sequence diagram looks like:

Before moving on with anything else, lets write a unit test for both of the move and quit commands. Start by creating a new Unit Test class called QuitCommandTest and enter the following:

using System;

using System.Collections;

using Mazegame.Control;

using Mazegame.Entity;

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace MazegameTest

{

[TestClass]

public class QuitCommandTest

{

private ParsedInput playerInput;

private Player thePlayer;

private CommandHandler handler;

private QuitCommand quit;

[TestInitialize]

public void Init()

{

playerInput = new ParsedInput("quit", new ArrayList());

thePlayer = new Player("greg");

handler = new CommandHandler();

quit = new QuitCommand();

}

[TestMethod]

public void TestQuit()

{

// test quit command no arguments

CommandResponse response = quit.Execute(playerInput, thePlayer);

Assert.IsTrue(response.FinishedGame);

Assert.IsTrue(response.Message.Contains("Goodbye"));

// test quit command >0 arguments

playerInput.Arguments = new ArrayList(new string[] {"this", "game"});

response = quit.Execute(playerInput, thePlayer);

Assert.IsTrue(response.FinishedGame);

Assert.IsTrue(response.Message.Contains("Goodbye"));

}

[TestMethod]

public void TestQuitHandler()

{

// test quit command no arguments

CommandResponse response = handler.ProcessTurn("quit", thePlayer);

Assert.IsTrue(response.FinishedGame);

Assert.IsTrue(response.Message.Contains("Goodbye"));

// test quit command >0 arguments

response = handler.ProcessTurn("quit this game", thePlayer);

Assert.IsTrue(response.FinishedGame);

Assert.IsTrue(response.Message.Contains("Goodbye"));

}

}

}

Run the test to make sure it works. Now create a Unit Test class called MoveCommandTest and enter the following:

using System;

using System.Collections;

using Mazegame.Control;

using Mazegame.Entity;

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace MazegameTest

{

[TestClass]

public class MoveCommandTest

{

private ParsedInput playerInput;

private Player thePlayer;

private CommandHandler handler;

private MoveCommand move;

private Location t127;

private Location gregsoffice;

[TestInitialize]

public void Init()

{

playerInput = new ParsedInput("move", new ArrayList());

thePlayer = new Player("greg");

t127 = new Location("a lecture theatre", "T127");

gregsoffice = new Location("a spinning vortex of terror", "Greg's Office");

t127.AddExit("south", new Exit("you see a mound of paper to the south", gregsoffice));

gregsoffice.AddExit("north", new Exit("you see a bleak place to the north", t127));

thePlayer.CurrentLocation = t127;

handler = new CommandHandler();

move = new MoveCommand();

}

[TestMethod]

public void TestMoveNowhere()

{

Assert.AreSame(t127, thePlayer.CurrentLocation);

// test move command no arguments

CommandResponse response = move.Execute(playerInput, thePlayer);

Assert.IsFalse(response.FinishedGame);

Assert.IsTrue(response.Message.Contains("no exit there"));

Assert.AreSame(t127, thePlayer.CurrentLocation);

}

[TestMethod]

public void TestMoveNoExit()

{

playerInput.Arguments = new ArrayList(new string[] { "west" });

CommandResponse response = move.Execute(playerInput, thePlayer);

Assert.IsFalse(response.FinishedGame);

Assert.IsTrue(response.Message.Contains("no exit there"));

Assert.AreSame(t127, thePlayer.CurrentLocation);

}

[TestMethod]

public void TestTakeExit()

{

playerInput.Arguments = new ArrayList(new string[] { "south" });

CommandResponse response = move.Execute(playerInput, thePlayer);

Assert.IsFalse(response.FinishedGame);

Assert.IsTrue(response.Message.Contains(gregsoffice.Description));

Assert.AreSame(gregsoffice, thePlayer.CurrentLocation);

}

[TestMethod]

public void TestMoveHandler()

{

// test move command no arguments

CommandResponse response = handler.ProcessTurn("move to the south", thePlayer);

Assert.IsFalse(response.FinishedGame);

Assert.IsTrue(response.Message.Contains(gregsoffice.Description));

Assert.AreSame(gregsoffice, thePlayer.CurrentLocation);

}

}

}

Build your solution and try to run your test. You will see that the TestMoveNowhere test fails. This problem occurs because we have invoked the move command with no arguments and our arguments ArrayList in ParsedInput is empty.

We can fix this easily with a bit of defensive coding in MoveCommand as follows:

namespace Mazegame.Control

{

public class MoveCommand : Command

{

public override CommandResponse Execute(ParsedInput userInput, Player thePlayer)

{

if (userInput.Arguments.Count == 0)

{

return new CommandResponse("If you want to move you need to tell me where");

}

String exitLabel = (String) userInput.Arguments[0];

Exit desiredExit = thePlayer.CurrentLocation.GetExit(exitLabel);

if (desiredExit == null)

{

return new CommandResponse("There is no exit there.. Trying moving someplace moveable!!");

}

thePlayer.CurrentLocation = desiredExit.Destination;

return new CommandResponse("You find yourself looking at " + thePlayer.CurrentLocation.Description);

}

}

}

Now modify the test method accordingly:

[TestMethod]

public void TestMoveNowhere()

{

Assert.AreSame(t127, thePlayer.CurrentLocation);

// test move command no arguments

CommandResponse response = move.Execute(playerInput, thePlayer);

Assert.IsFalse(response.FinishedGame);

Assert.IsTrue(response.Message.Contains("tell me where"));

Assert.AreSame(t127, thePlayer.CurrentLocation);

}

You should be able to run the test without incident.

Now our tests are running give your game a little play test, to verify it is behaving how you would like:

I’m still not quit happy with the information I get from the game when I move from one location to another. So I am going to change how the results are presented. Let’s start by developing a ToString method for Location.

using System;

using System.Collections;

using System.Text;

namespace Mazegame.Entity

{

public class Location

{

private Hashtable exits;

private String description;

private String label;

public Location()

{

}

public Location(String description, String label)

{

Description = description;

Label = label;

exits = new Hashtable();

}

public Boolean AddExit(String exitLabel, Exit theExit)

{

if (exits.ContainsKey(exitLabel))

return false;

exits.Add(exitLabel, theExit);

return true;

}

public Exit GetExit(String exitLabel)

{

return (Exit) exits[exitLabel];

}

public String Description

{

get { return description; }

set { description = value; }

}

public String Label

{

get { return label; }

set { label = value; }

}

public String AvailableExits()

{

StringBuilder returnMsg = new StringBuilder();

foreach (string label in this.exits.Keys)

{

returnMsg.Append("[" + label + "] ");

}

return returnMsg.ToString();

}

public override string ToString()

{

return "**********\n" + this.Label + "\n**********\n"

+ "Exits found :: " + AvailableExits() + "\n**********\n"

+ this.Description + "\n**********\n";

}

}

Now I can change my MoveCommand class accordingly:

namespace Mazegame.Control

{

public class MoveCommand : Command

{

public override CommandResponse Execute(ParsedInput userInput, Player thePlayer)

{

if (userInput.Arguments.Count == 0)

{

return new CommandResponse("If you want to move you need to tell me where");

}

String exitLabel = (String) userInput.Arguments[0];

Exit desiredExit = thePlayer.CurrentLocation.GetExit(exitLabel);

if (desiredExit == null)

{

return new CommandResponse("There is no exit there.. Trying moving someplace moveable!!");

}

thePlayer.CurrentLocation = desiredExit.Destination;

return new CommandResponse("You successfully move " + exitLabel + " and find yourself somewhere else\n\n" + thePlayer.CurrentLocation.ToString());

}

}

}

So the results now look as follows:

Rerunning the tests reveals nothing is broken, so now we can move on to developing the next user story.

Look Command

Before we implement the look command we should specify how we think it should behave by specifying its user story(s). Our initial RAD identified 3 “look” stories

· Look Item

· Look Character

· Look Location

I can think of a third as well “look exit”. As we haven’t introduced non-playing characters or items yet let’s focus on location and exit for now:

· Look Location – the player issues the look command without arguments and the system returns a description of the current player location.

· Look Exit – the player issues the look command with an argument and if the argument matches an exit a description of the exit is returned.

Both of these user stories are related and can be completed at once by developing the LookCommand:

namespace Mazegame.Control

{

public class LookCommand : Command

{

private CommandResponse response;

public override CommandResponse Execute(ParsedInput userInput, Player thePlayer)

{

response = new CommandResponse("Can't find that to look at here!");

if (userInput.Arguments.Count == 0)

{

response.Message = thePlayer.CurrentLocation.ToString();

return response;

}

foreach (string argument in userInput.Arguments)

{

if (thePlayer.CurrentLocation.ContainsExit(argument))

{

Exit theExit = thePlayer.CurrentLocation.GetExit(argument);

return new CommandResponse(theExit.Description);

}

}

return response;

}

}

}

To get this to work I need to introduce a new method in Location called ContainsExit:

public bool ContainsExit(String exitLabel)

{

return exits.Contains(exitLabel);

}

And now I need to add the LookCommand to my CommandHandler:

private void SetupCommands()

{

availableCommands.Add("go", new MoveCommand());

availableCommands.Add("quit", new QuitCommand());

availableCommands.Add("move", new MoveCommand());

availableCommands.Add("look", new LookCommand());

}

Compile this and give it a try!

Now we have play tested this we need to finish by writing a unit test for the LookCommand:

using System;

using System.Collections;

using Mazegame.Control;

using Mazegame.Entity;

using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace MazegameTest

{

[TestClass]

public class LookCommandTest

{

private ParsedInput playerInput;

private Player thePlayer;

private CommandHandler handler;

private LookCommand look;

private Exit southExit;

private Location t127;

[TestInitialize]

public void Init()

{

playerInput = new ParsedInput("look", new ArrayList());

thePlayer = new Player("greg");

t127 = new Location("a lecture theatre", "T127");

Location gregsoffice = new Location("a spinning vortex of terror", "Greg's Office");

southExit = new Exit("you see a mound of paper to the south", gregsoffice);

t127.AddExit("south", southExit );

thePlayer.CurrentLocation = t127;

handler = new CommandHandler();

look = new LookCommand();

}

[TestMethod]

public void TestLookNowhere()

{

Assert.AreSame(t127, thePlayer.CurrentLocation);

// test move command no arguments

CommandResponse response = look.Execute(playerInput, thePlayer);

Assert.IsFalse(response.FinishedGame);

Assert.IsTrue(response.Message.Contains(t127.Description));

}

[TestMethod]

public void TestLookNoMatch()

{

playerInput.Arguments = new ArrayList(new string[] {"bunyip"});

CommandResponse response = look.Execute(playerInput, thePlayer);

Assert.IsFalse(response.FinishedGame);

Assert.IsTrue(response.Message.Contains("Can't find that"));

}

[TestMethod]

public void TestLookExit()

{

playerInput.Arguments = new ArrayList(new string[] {"south"});

CommandResponse response = look.Execute(playerInput, thePlayer);

Assert.IsFalse(response.FinishedGame);

Assert.IsTrue(response.Message.Contains(southExit.Description));

}

[TestMethod]

public void TestMoveHandler()

{

// test move command no arguments

CommandResponse response = handler.ProcessTurn("look to the south", thePlayer);

Assert.IsFalse(response.FinishedGame);

Assert.IsTrue(response.Message.Contains(southExit.Description));

}

}

}

Sequence Command

The look command is illustrated below as a sequence diagram. Not the use of “fragment” boxes to illustrate condition messages.

Page 1 of 17

Page 4 of 17

sd Look Command:CommandHand...:LookCommand:ParsedInput:Player:Location:Exit:CommandResponsealt [command arguments = 0][command arguments > 0]alt [exit found]Execute(ParsedInput, Player) :CommandResponsecreate()CurrentLocation() :LocationToString() :location descriptionMessage(location description)Arguments() :list of arguments[for each argument]:ContainsExit(argument) :boolGetExit(String) :ExitDescription() :stringMessage(exit description)

sd Process Turn:DungeonMaster«interface»:IMazeClient:Parser:ParsedInputprocessMove(ParsedInput)GetCommand() :StringParse(String) :ParsedInputCommand() :string[command not recognised]:PlayerMessage(String)[command recognised]:processCommand()

sd Process Turn:DungeonMaster«interface»:IMazeClient:CommandHand...:CommandResponsehandlePlayerTurn() :boolGetCommand() :StringProcessTurn(String, Player) :CommandResponseMessage() :stringPlayerMessage(String)