FX My Life XIII: Then Again, Maybe I AM Dyslexic

I feel like we’re coming to the end of our tic-tac-toe adventure. There are some graphical niceties to add in, maybe an “AI”, some polish, etc. But I notice that my “dyslexia” has struck again. I’ve mixed up the columns and rows for the drop-check code such that…well, look:

You see, we have the “not” symbol in the first column, second row and, I can assure you, we have an okay to drop on the second column, first row.

It’s kind of fun to do this simple stuff and see how the need for various things evolved. We’ve seen, e.g., how reactive programming is useful, how a paradigm for communication is vital, the value of separation of concerns, and what an awful good idea it is to have tests.

Before I fix this, I want to make it so that a request to replace a piece on the board, which right now is doable, is not allowed. In fact, let’s beef up the TicTacToe class so that it isn’t just a passive recipient of whatever the UI throws at it.

So, let’s change our addPiece method so that it throws an error:

public void addPiece(String piece, int x, int y) throws Exception {
if (state[y][x] != null) {
throw new Exception("Attempt to add piece illegally!");
}
state[y][x] = piece;
checkGameState();
}

Now, this also reflects an interesting conundrum that we run into every day when programming. Strictly speaking, the UI should ask whether it’s okay to drop a piece (and actually it does ask, but we’ve screwed that up) and should never try to drop a piece where it’s not allowed.

So, this code, strictly speaking, shouldn’t be necessary. Whether or not we should add it anyway really depends on a few things: Is it likely to happen? Is it likely to happen in a way that we don’t immediately catch and fix (so that it doesn’t end up in production)? Is it likely to happen in a way that makes it hard to figure out what happened? What’s the severity of the impact if it does happen? Are there likely future situations which might be unexpectedly impacted by it happening? (Hold on to this last thought.)

If we have a modest amount of testing for this very modest project, this isn’t something that’s likely to happen in a way that we can’t immediately fix or figure out what happened, and the severity is basically null, since we’re writing a silly game. Unless our tic-tac-toe games goes viral and millions of fans discover this “exploit”, it’s a non-issue.

Our “wall” to prevent this can be on the user end: Just don’t let the user make an illegal move. But I’m going to leave this code in. (In a more real-world situation I would be more inclined to take it out because more code is more maintenance.)

Now, if we fix our handleOnDragOver:

public void handleOnDragOver(DragEvent event) {
int column = getCoord(board.getWidth(), event.getX(), board.getRowCount());
int row = getCoord(board.getHeight(), event.getY(), board.getColumnCount());

if (game.get(row, column) == null) {
if (event.getGestureSource() != board && event.getDragboard().hasString()) {
event.acceptTransferModes(TransferMode.COPY_OR_MOVE);
}
}
event.consume();
}

That exception will never be thrown — as long as I don’t screw that part up again. Also, we can use it to determine whether a piece is okay to drop based on whose turn it is. Let’s add a field to TicTacToe:

public String turn;

Then in NewGame, we add:

turn = "X"; //X always goes first

Now, addPiece becomes:

public void addPiece(String piece, int x, int y) throws Exception {
if (state[y][x] != null || !turn.equals(piece)) {
throw new Exception("Attempt to add piece illegally!");
}
state[y][x] = piece;
checkGameState();
if (turn.equals("X")) {
turn = "O";
} else {
turn = "X";
}
}

And if we try to place an “X” when it’s “O”s turn, we get our lovely stack trace.

java.lang.Exception: Attempt to add piece illegally!
at fxgames.TicTacToe.addPiece(TicTacToe.java:83)
at fxgames.TttController.handleOnDrop(TttController.java:204)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:64)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:564)
at com.sun.javafx.reflect.Trampoline.invoke(MethodUtil.java:76)
at jdk.internal.reflect.GeneratedMethodAccessor2.invoke(Unknown Source)
at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.base/java.lang.reflect.Method.invoke(Method.java:564)

This brings up another relevant issue.

By the way, if you’ve been loading and saving games, you may notice that some of the older save games become “incompatible” as you change things in the underlying class. In this case, adding the “turn” field probably breaks the old versions of the serialized class.

The big struggle in OO programming is always where to draw the line. Who is responsible for what. But even if you’re not doing OO, boundaries quickly become an issue. Whether we have an object or a bunch of functions designed to handle tic-tac-toe related issues, the question of responsibility arises.

A strong argument, for example, can be made that “addPiece” should just add a piece, with no regard for what’s already there, or whose turn it is or whether the game is over. A board should just be a grid for holding pieces, and we should have another object that deals with issues of legality, as it were.

In fact, we’ll certainly look at this in later games.

But even if you separate the physical board from the logical game, you now have the question of the interface, and now you have at least three layers. It reminds one of the old saw on the meanings of “can” and “can’t”:

A man calls his lawyer after being accused of a crime and his lawyer says, “They can’t throw you in jail for that!” and he responds with, “I’m calling from jail!”

Or the flipside: “They can throw your elephant in jail for that!”

The former reflects a legal impossibility contradicted by a physical reality, while the latter reflects a physical impossibility that is legally untenable.

As it appertains to programming board games, we could note that some versions of computerized board games allow you to overturn the table. We’ll be happy enough with our tic-tac-toe class not allowing an illegal game state. Why is that important? Well, remember when I said:

Are there likely future situations which might be unexpectedly impacted by it happening? (Hold on to this last thought.)

The future situation, which we’ll tackle in the next post, is that of trying to build a computer player. I won’t say “AI” because a genuine “AI” wouldn’t have this problem, but the code we’ll end up writing will almost certainly depend on the board always being in a legal state.

In the meantime, the boundary question we have to tackle is: Should the UI allow for Xs and Os to be grabbed willy-nilly, and tell the users when they’ve tried to make an illegal move, or should the UI prevent illegal moves.

The former can be a source of irritation, when a program allows you to go through motions only to tell you those motions were stupid and disallowed.

The latter can be a source of irritation, when a program disallows an action, and you don’t really know why. Or sometimes programs will actually hide actions, and that will be frustrating because you don’t know if it’s because the action is disallowed or maybe you’re looking in the wrong place or what. (Big ol’ deeply nested classic menu structures are great at this: “Am I not able to print, or am I looking in the wrong menu for the print option?”

Let’s do this: For now, we’ll allow the player to pick up whatever piece, but we’ll warn him about it. To do this, I think it would be good to switch the whole board over to a BorderPane, like so:

A BorderPane has five areas for putting controls: top, right, bottom, left and center. So I put the game info in the top section, the “pieces” area on the right, and added a label to the bottom which we will use to warn the player about the folly of his ways.

For all its limitations, SceneBuilder makes this super-easy. We can wrap the whole thing board in the border-pan, then drag and drop the h-box, the group, and the grid pane into the top, center and right spaces accordingly. Our new structure:

Now, there is an interesting artifact that arises. If we had the grid pane directly in the “center” area, it would expand to fill out the entire area no matter how much the BorderPane was resized. But the Group just keeps it at its preferred size, no scaling, and leaves a lot of space around it.

When the board Grid is encased in a Group.
And when it’s not.

Remember, also, that we have to change the OuterGroup from a Vbox type to a BorderPane.

@FXML
public BorderPane outerGroup;

This is kind of annoying. We don’t really care what kind of object outerGroup is as long as it has basic functionality. Can we just make it, I don’t know, a Node? That way, when we change the type around, we don’t have to fix the code.

@FXML
public Node outerGroup;

Looks like we can! Great! Now we can fool around to our heart’s content with the FXML.

Anyway, let’s give the message an fx:id of “message” and hook it up, so that we can change the text in handleOnDragDetected:

public void handleOnDragDetected(MouseEvent event) {
String piece = ((ImageView) event.getSource()).getId();
message.setText(piece + " is being dragged!");
Dragboard db = X.startDragAndDrop(TransferMode.ANY);

ClipboardContent content = new ClipboardContent();
content.putString(piece);
db.setContent(content);
}

Now, what we should probably do is say if the piece doesn’t match with the player whose turn it is, warn the user and disallow the drag.

public void handleOnDragDetected(MouseEvent event) {
String piece = ((ImageView) event.getSource()).getId();
if (!game.winner.equals("")) {
message.setText("The game is over. Press NEW for a new game.");
} else {
if (!piece.equals(game.turn)) {
message.setText("You can't move " + piece + " when it's " + game.turn + "'s turn!");
} else {
message.setText(piece + " is being dragged!");
Dragboard db = X.startDragAndDrop(TransferMode.ANY);

ClipboardContent content = new ClipboardContent();
content.putString(piece);
db.setContent(content);
}
}
}

This actually prevents the UI from making the illegal requests in the first place, which is probably for the best.

And while we’re at it, let’s set the message to show a winning message:

public void drawVictorySlash() {
if (!game.winner.equals("")) {
message.setText("Game over! " + game.winner + " wins!");
var bw = board.getWidth();
...

Ok, that’s enough for now. Let’s go another round for an “AI”, just for giggles, then maybe one more on the graphics, then we’ll start on another game.

I am a poor, wayfaring stranger, traveling through this world of woe.