When I first came up with a complete program here I was extremely happy. Functional programming feels like insurmountable (less so now but still) and building a fully functional program in F# felt great. Unfortunately, the moment has passed very quickly! Now it’s time to revisit and improve the Rock-Paper-Scissors-Lizard-Spock game. My main goal now is add
- Convert the game into a class
- Add exception handling to handle invalid user input
- Add unit tests
So first I had to investigate a few concepts to accomplish these goals. PluralSight course on F# was a very helpful resource.
First version was not testable because it generated random moves inside the game and there was no way of anticipating the outcome. So I needed to harness some constructor Dependency Injection like this.
type RPSLS(userInput: string, moveGenerator: IMoveGenerator) = let mutable playerScore = 0 let mutable computerScore = 0 let moveGen = moveGenerator
When the game is run from the console application the RPSLS object is created with a random move generator as usual so the gameplay has not been affected. But now the tests can build the object with a FakeMoveGenerator that implements the same IMoveGenerator interface.
So the actual move generator the game uses becomes a separate object like this:
type RandomMoveGenerator() = member this.GenerateMove(n) = (this :> IMoveGenerator).GenerateMove(n) interface IMoveGenerator with member this.GenerateMove(n) = let rnd = System.Random() let output = [ for i in 1 .. n -> let index = rnd.Next(0, 5) match index with | 0 -> Move.Rock | 1 -> Move.Paper | 2 -> Move.Scissors | 3 -> Move.Lizard | 4 -> Move.Spock | _ -> failwith "Unexpected move" ] output
and the one tests use is like this:
type FakeMoveGenerator(moves : List<Move>) = let mutable moveList = moves member this.MoveList with get () = moveList and set (value) = (moveList <- value) member this.GenerateMove(n) = (this :> IMoveGenerator).GenerateMove(n) interface IMoveGenerator with member this.GenerateMove(n) = let output = [ for i in 1 .. n -> moveList.Item(i-1) ] output
And they both implement the same interface IMoveGenerator:
type IMoveGenerator = abstract member GenerateMove : int -> List<Move>
Another benefit of this is the game can be improved easily with advanced move generators. For example a move generator can be implemented to generate specific move sequences. There is an advanced strategy guide here which is an interesting read.
Installing xUnit and xUnit Runner
Installing xUnit is pretty straightforward. Just use NuGet and add the package. For runner, apparently there was VS etension that needed to be installed separately but it’s no longer the case. Check out this guide to find out how to install xUnit test runner. It makes
I added a library to project for my tests, added xUnit and xUnit runner from NuGet I was ready to go.
So by decorating the test methoids with [
[<Fact>] let Game_Ends_With_Correct_Output_3_Moves_0_to_1() = let fakeGen = new FakeMoveGenerator([Move.Rock; Move.Rock; Move.Paper]) let newGame = new RPSLS("r r r", fakeGen) newGame.RunGame() Assert.Equal(0, newGame.PlayerScore) Assert.Equal(1, newGame.ComputerScore)
As the game is created with the user input I wanted it to check the user input before it ran the game and throw an exception if the input was erroneous. Throwing an exception is carried out with failwith keyword. That’s straightforward. Handling it on the other hand came with a little surprise:
There is a try…with block which is corresponds to standard try…catch. And there is a try…finally block but they is no try…catch…finally block so they had to be used separately:
[<EntryPoint>] let main argv = try try let userInput = Console.ReadLine() let newGame = new RPSLS(userInput, new RandomMoveGenerator()) newGame.RunGame() with _ -> printfn "An error occured" finally printfn "Press any key to quit" Console.ReadKey() 0
In order to accomplish what I set out for I had to use two nested try blocks. Can’t say it looks great but until I get more accustomed with it I’ll just go along with quirks. At the moment it’s entirely possible that there’s a better alternative so I hope that’s the case here. Either way, it does the job after all.
Final version is on GitHub. I might set sail to other seas and start new small projects before I revisit this one.