FX My Life X: Reset

Blake
10 min readMay 5, 2021

So, the first thing we want to do with our tic-tac-toe board, administratively speaking, is allow it to be reset. Although the meat of this is not specifically FX, because we’re just resetting values, it does cross over in some ways. I have done this whole thing in a fairly sloppy fashion, because I find that doing things fast — even though “wrong” — is a great way to get information on how to do them “right”.

When we get out of the rarified atmosphere of math, there is no real “right” and “wrong”, only “righter” and “wronger”.

So, we have this frame set up very early on with some buttons on it.

I very deliberately made this separate from everything else because I wanted to see how we could communicate hither and yon in an FX app. And this, at first glance, looks potentially very simple indeed.

Simple in a good way, I mean.

Now, the “back” button, which we implemented earlier, was “easy”, at least as far as getting the communication done because it doesn’t actually communicate with the node in question. It just says, “Whatever node this is, do this with it.” Which is a kind of ideal state, really. In theory, when we move on to the next game, we’ll be able to slide it in and out without any friction whatsoever. (And I actually believe this to be true, which is rare.)

But there’s no way to deal with “New”, for example, without being able to talk to the Tic-Tac-Toe controller. We really don’t want the main Controller to have to know about any of the other controllers, but it has to be able to send some kind of message to the active node. Enter interfaces.

We will say that any node that wants to be influenced by the outer buttons has to be able to…do a thing. Specifically:

package fxgames;

import javafx.event.ActionEvent;

public interface framedWindow {
public boolean doThing(ActionEvent e); //return true if you did the thing and no more things need to be done
}

Then, in Controller, we’ll create a command that gets fired whenever “New”, “Load” or “Save” is clicked:

public void issueCommand(ActionEvent e) {
var n = nodeController.getActive();
if (n instanceof framedWindow) {
((framedWindow) n).doThing(e);
}

So, if one of the buttons is fired, we’ll just pass the event along to a child. Since all (one) of our children will implement framedWindow, we don’t really need to check the instanceof but, I dunno, if we’re stuck in the land of static typing, it seems like we should use type checking. It might save us from a bug. (It might also create a worse bug. Que sera sera.)

Of course, as lovely and simple as this is, it doesn’t work. Because we have the node not the controller. Furthermore, there’s no way to get the controller from the node. If we believe this SlackOverflow, we can rig things up to return the controller, but the method shown breaks SceneBuilder.

Ain’t that a kick in the head?

This is frustrating, but it makes sense. And presumably as we get more involved examples, we’ll end up with situations where we don’t want hard-coding between controllers and nodes, or at least that’s what we’ll tell ourselves for now to keep from plunging into despair.

Let’s see what the debugger says:

OK, so, the active node is our outerGroup. Seems like we can use this, if we can find a way to associate a node with a controller. So, let’s put another hash map into NodeController:

private final HashMap<Node, Object> controllerMap = new HashMap<>();
protected void addController(Node node, Object controller) { controllerMap.put(node, controller);}
public Object getController(Node node) {return controllerMap.get(node);}

Besides assigning variables in our class from FXML files, the FXML loader will also assign an initialize method, which we can exploit:

@FXML
public void initialize() {
NodeController.me.addController(outerGroup, this);
}

Now, we can use the node to get its controller back in our Controller class:

public void issueCommand(ActionEvent e) {
var n = nodeController.getActive();
var c = nodeController.getController(n);
if (c instanceof framedWindow) {
((framedWindow) c).doThing(e);
}

In TttController, we’ll now see the event passed in:

@Override
public boolean doThing(ActionEvent e) {
System.out.println(e);
return false;
}

Well, hell, do we even need the interface at this point? Let’s try this again, without. Replace the controllerMap with a handlerMap:

private final HashMap<Node, EventHandler<ActionEvent>> handlerMap = new HashMap<Node, EventHandler<ActionEvent>>();
protected void addHandler(Node node, EventHandler<ActionEvent> eh) { handlerMap.put(node, eh);}
public EventHandler<ActionEvent> getHandler(Node node) {return handlerMap.get(node);}

Now we’ve got a HashMap of nodes, like before, but they point to event handlers that can handle ActionEvents. I am starting to think that working in Java is the cure for someone who has faith in static typing, given EventHandler<ActionEvent> having to appear everywhere.

And this is the most basic use of generics imaginable. Oy.

Anyway, our controller issueCommand method can now look like:

public void issueCommand(ActionEvent e) {
var n = nodeController.getActive();
var h = nodeController.getHandler(n);
if (h != null) {
h.handle(e);
}
}

We can even take it in a notch:

public void issueCommand(ActionEvent e) {
var h = nodeController.getHandler(nodeController.getActive());
if (h != null) {
h.handle(e);
}
}

We can’t quite get rid of the declaration portion, which is kind of funny, because Java can infer the type from our var h = nodeController... but not if it’s in the if statement, I guess.

public void issueCommand(ActionEvent e) {
EventHandler<ActionEvent> h;
if ((h = nodeController.getHandler(nodeController.getActive())) != null) {
h.handle(e);
}
}

Meh. Some day.

Now, our TttController initialize looks like this:

@FXML
public void initialize() {
NodeController.me.addHandler(outerGroup, (e) -> {System.out.println(e);});
}

Wow. Long way to go to…y’know…handle a button event. And we may not be done yet. Because we now have our event with no good way to react on it.

What does our println put out?

javafx.event.ActionEvent[source=Button@1f733b8b[styleClass=button]'_New']

Well, hell. I suppose we could try to pull out what to do from the text but that breaks the instant we try to go international. JavaFX allows nodes to have custom properties, but I don’t see any way to set those up in SceneBuilder.

SceneBuilder seems to have a dodgy reputation when it comes to fxml that the user has tinkered with, so I’m not keen to get in there and set the custom properties manually. Fortunately, there’s also an “id” code, separate from the fx:id code, and we can use that.

NodeController.me.addHandler(outerGroup, (e) -> {
var id = ((Button) e.getTarget()).getId();
switch (id) {
case "new":
System.out.println("new");
break;
case "load":
System.out.println("load");
break;
case "save":
System.out.println("save");
break;
default:
System.out.println("what?");
}
});

Sure enough, if we set the “id” of the new button to “new” and then click on it, we’ll get “new” printed out. If you’re like me, you’ll forget to set the ids for the other two, and Java will except-the-hell-out when you click on “load” and “save”.

That seems sub-optimal, so:

NodeController.me.addHandler(outerGroup, (e) -> {
var id = ((Button) e.getTarget()).getId();
if (id != null) {
switch (id) {
case "new":
System.out.println("new");
break;
case "load":
System.out.println("load");
break;
case "save":
System.out.println("save");
break;
default:
System.out.println("what?");
}
}
});

Testing for null.

Now, Where Was I?

We could worry about the organization of the code at this point, but let’s just slap this stuff in our spiffy classic-C style switch statement (complete with breaks!) so we can actually get done what it was we wanted to get done.

Actually, before we do that, let’s draw the line properly for the victor, not always top-left to bottom-right. That’s currently in our handleOnDrop method and should be in check .

public void check(int x, int y, int xd, int yd) {
if (gameOver) return;
String s = bstr(x, y, xd, yd);
if (s.equals("OOO") || s.equals("XXX")) {
gameOver = true;
winner = s.substring(0, 1);
wx = x;
wy = y;
wxd = xd;
wyd = yd;
var cw = board.getWidth() / 3;
var ch = board.getHeight() / 3;

outerGroup.getChildren().add(line);
line.getStyleClass().add("line");
line.setStartX(0);
line.setStartY(0);
line.setEndX(board.getWidth());
line.setEndY(board.getHeight());
}

I realized when doing this that I had made an error earlier on, one that I make so often I should probably look into mortification of the flesh to do penance. My habit, ingrained from an early age, is to always view grids as Data[X,Y] (or Data[X][Y]) where X is the horizontal axis (the column) and Y is the vertical axis (the row). I don’t think this is unusual or incorrect.

But data is almost invariably stored in rows. In other words, Data[X] is an array of rows (vertical). The second dimension is the columns in that row. Now, look, back in the day (and even today) you could store things as one big block and…

OK, look, the point is, I always get them backwards. Which I did here, in bstr which builds the string we use to check for victory. It’s an easy fix:

private String bstr(int x, int y, int xd, int yd) {
return state[y][x] + state[y + yd][x + xd] + state[y + yd + yd][x + xd + xd];
}

And doing that flip in this one place localizes the issue. Sorta. We still have it in handleOnDragOver and handleOnDrop, but I didn’t get it backwards there, since it would’ve been immediately obvious when the X or O showed up in the wrong place. (Or maybe I did get it wrong and fixed it.) A good universal fix would be to completely isolate game state into its own class, but we’re not there yet.

Anyhoo.

public void check(int x, int y, int xd, int yd) {
if (gameOver) return;
String s = bstr(x, y, xd, yd);
if (s.equals("OOO") || s.equals("XXX")) {
gameOver = true;
winner = s.substring(0, 1);

var bw = board.getWidth();
var bh = board.getHeight();
var cw = bw / 3;
var ch = bh / 3;
var wxs = 0.0;
var wys = 0.0;
var wxe = bw;
var wye = bh;

outerGroup.getChildren().add(line);
line.getStyleClass().add("line");
if (xd == 1 && yd == 1) {
//don't actually have to do anything since these are the defaults, but lets leave the case in here
} else if (xd == -1 && yd == 1 ) {
wys = bh;
wye = 0;
} else if (xd == 1) {
wys = (y * ch) + (ch /2);
wye = wys;
} else if (yd == 1) {
wxs = (x * cw) + (cw / 2);
wxe = wxs;
}
line.setStartX(wxs);
line.setStartY(wys);
line.setEndX(wxe);
line.setEndY(wye);
}
}

This seems to draw the lines in the right places, but it sure would be helpful if we could actually reset using the “New” button, then we wouldn’t have to quit the program every time we wanted to check.

Reset Reset

So, here’s the wrong way to do it.

case "new":
System.out.println("new");
state = new String[3][3];
winner = "";
line = new Line();
gameOver = false;
break;

You should try it, though. What will happen when you do this? Well, visually, nothing. You will be able to lay Xs and Os down anywhere, tho’.

See, our game’s state has no relation to the visual representation of the state at all. This is part of why reactive programming is so hot. If we’d set things up so that our board just knew how to draw itself based on the state, and was notified when the state was changed (rather than with interface events like dragging and dropping), we’d be done now.

But it’s actually pretty hard to learn something big, like JavaFX, while learning another big thing, like reactive programming. (There are JavaFX libraries that do this, however. And maybe we’ll get to those eventually.) We’ll learn JavaFX’s quirks better this way.

So, how do we reset the visual representation? Well, what if we just deleted everything?

board.getChildren().clear();

Huh. Removes the grid lines — I didn’t know those were children, and that raises a bunch of questions — but not the strikethrough (because we added that to outerGroup, remember?).

OK, so, removing things is easy if you have a reference to the things you want to remove, but we’re kind of spontaneously adding things to the board. Well, what if we went through all the items and erased them individually, if it was appropriate to do so?

I tried various things to get an iterator that would work with getChildren, to no avail. So I went with a classic loop. When iterating over children for the purposes of remove, the general rule is to eliminate from last to first. That is,

1 2 3 4  => Index is 3
1 2 3 => Index is 2
1 2 => Index is 1
1 => Index is 0

If you go forward, you have to be clever about the index.

1 2 3 4   => Index is 0
2 3 4 => Index is 1
2 4 => Index is 2, and there is no 2

I came up with this:

for (int i = board.getChildren().size() - 1; i>0; i--) board.getChildren().remove(i);

…which IntelliJ immediately offered to change to:

IntStream.iterate(board.getChildren().size() - 1, i -> i > 0, i -> i - 1).forEach(i -> board.getChildren().remove(i));

That doesn’t strike me as better. Whichever way you go, the results are…shocking:

becomes:

OK, not shocking, really but kind of a “huh” moment. I guess the grid lines are children for purposes of the clear() routine but not for iterating over getChildren. Go figure.

Now our “new” code looks pretty good.

case "new":
System.out.println("new");
state = new String[3][3];
winner = "";
outerGroup.getChildren().remove(line);
gameOver = false;
for (int i = board.getChildren().size() - 1; i>0; i--) board.getChildren().remove(i);
break;

Or at least not horrible. This should probably all be in a game initialization function, but we’ll take the “win” on this and call it a day.

Next up: More fun with admin!

--

--

Blake
0 Followers

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