Ventitre
Web TTT
Moving from Core Driven to UI Driven within TTT.
Uku and Mateu set me the task of making my version of TicTacToe available to play online. In a previous post I talked about the difficulties I had setting up the Play Framework with the IntelliJ. Unfortuantely the solution I put forward in that post turned up a little short of the mark.
In following my previous guide, I very quickly found myself in the same situation as before where IntelliJ was highlighting missing symbol errors, which prevented the project from being compiled. After several seaches online, I discovered, that in order to get framework support with IntelliJ, the Ultimate version is required. It was a frustrating solution and seemingly difficult to arrive at based on information available online. Neither the IntelliJ or Play Framework tutorials suggest that the Ultimate version is a pre-requisite.
The next issue was one of entirely my own doing, but certainly something worth
remembering. A view file with the name play.scala.html
is reserved. I assume this
is only when using some of the TypeFace Starter Templates like the one
I used “Java Play Seed”. Having a file with this name, creates a rather
difficult to debug cyclic dependency.
Admin done.
Now to talk about the real challenges that the change to web brought with it.
- My Java TTT was packaged as a single item with the Command Line User Interface as part of it.
- That package did not adhere to the package principles.
- The concept of a game loop does not work in the same way for Web as it does for CLI.
- Type checks are bad.
Working with the Command Line embedded package
This turned out to be a good and bad problem to have. It was bad because it transpired that a lot of justifications for having the UI passed around were only to fulfill requirements of the CLI version of the application. It was good because by quickly working around that UI, I was able to see with more clarity what the “Core” of the app really was.
The first step was to create a UserInterface Interface… this inappropriately named interface was used to replace all injections of the newly named CLI. This step then permitted me to build a MockUI element that implemented the new UI Interface. Since the requirement at this stage was just to reproduce Human vs Human, it was actually (although not necessarily advisable) to ignore the Human class altogether and just mark the board directly from the UI. This process gave some clarity to what was really required from “Core”, which was:
- Allow me to mark the board
- Tell me the current board state
- Tell me when the game is over
- Tell me why the game ended
Package Principles
Depend in the direction of stability.
Knowing now quite how little my UI really needed to accomplish I was able to start the process of dissecting my Java TTT into two, one with just the “Core” and another that would act as the CLI version of the app. I duplicated the original CLI version of the app, the plan being that the Core could be extracted rather than created. I then began carefully deleting classes that I felt were outside of the core’s responsibility. Once I had a bare bones Core version of the application, I imported it into the CLI version of the app, and started to remove the duplicated code. This was a slow process of systematically preventing compilation errors until I was back at a stage where the tests would run. At which point the priority was to ensure that no tests were lost in the transfer, and the functionality remained the same.
Due to the way the CLI was embedded in some of the core code, some of the logic had to move to other classes. Decisions were made in conjunction with my new understanding of the requirements for the Web version of the application.
Once this process was completed, a few responsibilities had moved around, but I had a fully functional command line version of the application, with the addition of a separate TicTacToe Core that was available to both the CLI and Web application.
Game Loop
The fundamental change between the CLI applications and almost any other implementations is the way that the game is driven.
The CLI version is unique in that it is powered by a loop that will prompt the user whenever input is required.
Until the game is over, continue to prompt for the next move.
This situation is unique, since in most other situations the UI drives the application. In as much as, the user is not “told” when their input is required, they can give direction to the application at any time they want and typically through a variety of means.
The key part here is that the command line prompt blocks the gameplay loop. Nothing can or will happen until the prompt is dealt with, and it is that concept that is less common in a web framework. It can be done, but if the only way you could interact with a website was through modals and dialog boxes, the experience would likely feel restrictive.
Avoiding Type Checks
I have already learnt that type checks are a code smell during my apprenticeship, and they should be eliminated through Polymorphism where possible.
The problem I had was that in the 3 major game types HvsH, HvsComp, CompvsComp, a different method was required to ask for the next move each time. Human vs Human games were relatively simple to implement on their own, since each cell selection corresponded to a HTTP request that wrote to the board, and then returned the new board state.
Again, in isolation a Computer vs Computer game would be relatively simple to produce too. By long polling the core, I could simply ask for the next move at a set interval and it would appear as though the two computers were playing each other.
What I needed was a way to allow both of the solutions to be combined simultaneously, so that regardless of the type of game chosen, and regardless of who’s turn it was, the game could progress.
The solution I have implemented for now uses a polymorphic approach mixed with
what I still think amounts to a type check but not explicitly. The
polymorphism comes from the fact that both Human and Computer players make
moves on the board through the same GET
method.
The game loop is then moved on via a scheduled page refresh during gameplay. This allowed computer players to make a move automatically after a Human player. However, it had the undesired side effect that during Human vs Human games, if a Human player didn’t make a move within the refresh window, then the board would attempt to make the board in exactly the same place as the previous player.
The method below has since been refactored, but in it’s explicit form below shows the process for avoiding this clash with Human player timeliness.
This play()
method is invoked during the aforementioned GET
request.
public Result play() {
MoveHelper.assignUserChoice(ui, request().queryString());
MoveHelper.makeNextMove(ui, currentGame, board);
return ok(game.render("Let's Play!", board.getCells(), ui.endGame(board), currentGame.isOver(board)));
}
public class MoveHelper {
public static void assignUserChoice(WebInterface ui, Map<String, String[]> request) {
if(request.get("position") != null) {
ui.setLastInput(Integer.valueOf(request.get("position")[0]));
}
}
public static void makeNextMove(WebInterface ui, Game game, Board board) {
if (game.getCurrentPlayer().choosePosition(board) != -1) {
game.takeTurn(board, game.getCurrentPlayer().choosePosition(board));
ui.setLastInput(-1);
}
}
}
public class WebInterface implements UserInterface {
private int lastInput = -1;
public void setLastInput(int position) {
this.lastInput = position;
}
@Override
public int getNumber() {
return lastInput;
}
...
}
I think I will find a cleaner solution than an arbitrary check for -1
, but
I have at least proved to myself the concept of how it could work. More on
this, next time.