We should have a way to save our game and subsequently, a way to load it, which would wrap up our tic-tac-toe adventure, but that I think necessitates some way to name the players. After all, it wouldn’t do to have Joe and Fred save their game only to have Faye and Jill come along later and write over it. So, back to the SceneBuilder!
But if we look at our basic board fxml:
We really didn’t give ourselves much in the way of slack, did we? That’s why we ended up doing the group pane in previous sections. We could put these controls on the backing board where we have our load/save/etc. buttons, but it seems like this is too game specific. Sure, a lot of games will have two players, but a lot might not. We could hide one of the players for single-player games, but what if we have a game with six or more players?
Yeah, it seems too game-specific to me. But this is not a problem. We wrapped our board before, let’s try wrapping it again. Let’s put a vbox between the group and the flow pane:
Now we can drop a label, text field, splitter, label text field on the hbox to capture the player names.
I don’t really want to mess too much with the looks, but that’s a big part of this experiment so…once more into the breach. Let’s set the alignment property of the hbox to center left. That should give us something like:
The separator, which was supposed to enhance things visually, actually just makes things look awkward.
It can be hard to remember what all the margin/padding/spacing options do, but playing around with them in the SceneBuilder (Layout tab) can help. This seems to be true:
- Margin is the relationship of the control with its owner. Increasing these will increase the distance from the control’s outer edges.
- Padding is the relationship of the control with its children. Increasing these will increase the distance the children are from the control’s inner edge’s.
- Spacing is what we want. It controls the relationship between the children.
If we set spacing to 20, we get:
This is not bad. The “Player1” text looks too close to the left edge, but that might not be the case in the app. If we save this and fire up the app:
Huh. I must’ve been fooling with the label CSS because this accidentally looks kinda good, or at least close to something potentially good. We don’t have any room for scores, though.
We could take out the labels, and put the “Player 1” and “Player2” text inside the text fields. That’s pretty modern. But when the player actually fields them in, it’s just going to say:
Who are these people? Are they players? Spectators? Is their relationship friendly or are they competitors? Perhaps bitter rivals?
OK, obviously Fred and Joe would know who they were but you get the idea. The prompt-inside-the-field is good but not always a replacement for a label. So, let’s just shrink the text fields down to make space for a score board.
We’ll add yet-another vbox in the new empty space, and slap down some labels for scores:
OK, looks good. Let’s save and bring it up in the oh, my God!
Well, hell. The actual appearance of this is pretty easily remedied. We just have to change the CSS for labels. It’s currently:
.label {
-fx-background-color: black;
-fx-text-fill: white;
-fx-padding: 10px;
}
If we change it like so:
.label {
-fx-background-color: lightgoldenrodyellow;
-fx-text-fill: black;
-fx-padding: 2px;
This looks better.
Note the way the score area on the right overlaps the buttons. This was actually the most alarming aspect of the previous display, but now that it’s a bit less dramatic, I wonder how big a problem it is.
The issue is that the window doesn’t have enough room to display all its parts. If I make the window taller, everything is fine:
Now, I would’ve thought that the center area of a border pane couldn’t intrude on the top pane, but that’s precisely the sort of problem that isn’t really addressed in books.
What’s more our tile panel doesn’t seem behave this way. That is, if you grow the window, the tile panel grows, and if you shrink it, it shrinks. It doesn’t go up into the button area — until it does! This is a big clue.
The JavaFX minimum size is generally set to “calculated”, which basically means “you figure out what’s the smallest size you’re going to be and if the user tries to shrink you beyond that, well, that’s on him.” It’s so noticeable in this case because the tic-tac-grid rather insists on itself, and I suspect the vboxes and hboxes also say, “Hey, I need enough space to show all my kids.” But their authority doesn’t extend to their parents, apparently, which creates the situation above.
This is a problem much like the margin/padding/spacing one, where the parent has an opinion, the children have opinions, and even the grandparents and grandchildren can get in on the act.
This could create problems in some situations, like if you ended up with overlapping text fields, and it became unclear what was being entered where, but for now, we’ll let the user do what he wants, no matter how much it offends our aesthetic sensibilities.
Setting The Player Names
So, if we’re going to save, we need the player names. To address the text fields and labels directly, the most efficient way, apparently, is to create them in your source file first:
@FXML
public TextField player1;
@FXML
public TextField player2;
@FXML
public Label p1label;
@FXML
public Label p2Label;
@FXML
public Label p1Score;
@FXML
public Label p2Score;
To be honest, if I had my druthers, I’d put all the FXML stuff like this:
@FXML public TextField player1;
@FXML public TextField player2;
@FXML public Label p1label;
@FXML public Label p2Label;
@FXML public Label p1Score;
@FXML public Label p2Score;
But I make it a rule not to argue with code-formatter defaults, and the guys who set the standards apparently love narrow lines with gobs of blank space to the right of our increasingly-wide monitors.
Anyway, once you’ve created them in your code, SceneBuilder will allow you to attach them to your objects. We’ll do so, and create a handler for changes in the text:
public void nameChange(KeyEvent e) {
which we’ll then attach to both text fields’ On Key Typed events. We have two issues at this point: Which text field is being changed, and how to actually change it. To get the text, we can take the event, which will be of KeyTyped, and get its text.
The fact that it is a KeyTyped event isn’t going to matter. The key thing is that the target is going to be a TextField, and the TextField’s getText() will contain the new value of the field. But the target is some base object type or interface or something, so we’ll need to typecast.
((TextField) e.getTarget()).getText()
So, that’s our new value, but how do we figure out what label to change? Well, there are probably many sophisticated routes we could take, but we’re just going to say “if it’s text field 1, change label 1, otherwise change label 2.”
public void nameChange(KeyEvent e) {
Label l = (e.getTarget() == player1) ? p1label : p2Label;
l.setText(((TextField) e.getTarget()).getText());
}
Well, it’s not a lot of code, but it does seem rather baroque. This is the sort of thing we’re going to want to manage more sophisticatedly in more elaborate apps, though. I especially like this line:
l.setText(((TextField) e.getTarget()).getText());
The first objection people generally raise to Lisp languages is what? All the parentheses! And here we have five sets to say:
property2 = property1;
What a country!
Hugs and Kisses
As long as we’re providing functionality, we should make it possible for the players to choose whether they’re “X” or “O”. Looking at it now, it seems like having the Player 1 and Player 2 boxes be side-by-side is wasteful of vertical space.
Currently, we have a vbox that contains an hbox with our player/score info and a flow pane underneath with our actual grid. What if we put a vbox in the hbox and put all the label and text field stuff there?
If we do this, in other words:
What do we get?
Actually saved and used in the app, this doesn’t look bad:
Maybe some space in between the two player name label-text combos. But here’s an interesting fact: Labels can have child controls. If we drop each text into its associated label, and set the label’s Content Display to RIGHT, we get:
I’m going to set the spacing to about 15, and the top padding to 15 as well, to center the controls a little bit.
Now, I’d also like to have an “X” toggle. This would be a sliding button that had an “X” on the top and an “O” on the bottom. Something like this:
The “O” would line up with the Player 1 entry and the “X” would line up with Player 2. If the toggle were clicked, the “X” would slide up to line up with Player 1 and an “O” would be revealed where Player 2 is.
So, there is a toggle button in JavaFX, which is displayed on the control palette in SceneBuilder like so:
Which, apart from orientation, is pretty much what I’m looking for. But when you put a toggle button on a form, it comes out like this:
And the “toggling” consists of showing it as depressed. Well, perhaps we can get what we want messing with the properties.
Spoiler: We can’t. SceneBuilder lies with its little mini-button graphic. The ToggleButton is just a variant of a regular radio button that has a pop-up/depressed representation depending on whether its selected or not.
I’m not quite ready to do the new control thing yet, although it does seem to be fairly easy in JavaFX. What if I just put a regular button in there, rotated it 90 degrees, and gave it a text of “X__________O”. (Without the underscores. Medium apparently despises more than one consecutive space.)
Hmm. Well, I could make it transparent so it wasn’t so obviously a big, fat button. Or flatten it.
Interesting discovery: When you rotate a button 90 degrees, the padding/spacing seems to stay oriented around the non-rotated version. That is, if you try to make it taller, the button ends up getting wider. That’s how this ended up so big.
We’re fighting too hard to make controls do things they don’t want to do, so I think I’ll just put a good ol’ image-view alongside the player names. Comes out something like this:
Now we call this ImageView XoverO (forgive my unconventional capitalization), and hook up its Mouse Clicked event to some simple code:
public void togglePlayerX() {
playerOneIsX = !playerOneIsX;
if (playerOneIsX) {
XoverO.setRotate(0);
} else {
XoverO.setRotate(180);
}
}
The game state item will be playerOneIsX. We’re not actually respecting that in any way, of course, but that’s a separate issue and shouldn’t be affected by the saving/loading feature.
It might be fun to allow a meta-game where players could surreptitiously or using a limited resource, switch sides. That probably isn’t enough to make tic-tac-toe fun by itself, but we could envision a tic-tac-toe game with, let’s say, multiple boards and time limits…
That’s for another time. Let’s get back to the goal, which was just saving and loading the game.
Serializing and Saving
And speaking of things — this transition was a lot smoother before I added the previous section on toggle buttons — one doesn’t have to do in a Lisp language: Serializing objects in order to be able to persist their states. Although it’s only fair to point out that in the ultimate object-oriented language, Smalltalk, you don’t even have to save things because your objects just are. They exist in the primordial soup of the VM until you say otherwise. (Though the VM is serialized in case you need to re-create it.)
And, honestly, we shouldn’t make too much ado about this because, really, we don’t have to serialize much of anything. We just need to save a few things: The player names, the scores, and the current board state.
Welp. We ain’t gonna win any beauty contests with this but:
case "save":
FileChooser fileChooser = new FileChooser();
FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter("tic-tac-toe files (*.ttt)", "*.ttt");
fileChooser.getExtensionFilters().add(extFilter);
File file = fileChooser.showSaveDialog(Main.me.stage);
if (file != null) {
try {
file.createNewFile();
FileWriter writer = new FileWriter(file);
writer.write("Version 1");
writer.write(player1.getText());
writer.write(player2.getText());
writer.write(playerOneWins);
writer.write(playerTwoWins);
writer.write(Ties);
writer.write(String.valueOf(state));
writer.close();
} catch (IOException ioException) {
ioException.printStackTrace();
}
}
break;
So, we create a file chooser, show it, if we get a file back we create the file, and then write all our data to it.
In this, I’m not sure if the file.createNewFile will work if the file exists, and I’m not sure we’ll be able to read the “state” value back as easily as we wrote it. Since we’re writing binary, the file’s gonna be a little tricky to look at, too, but I didn’t want to convert and from strings for everything.
Honestly, the most usual way I’d do something like this is to store it all in key-value map and then just read and write the map. This is truly an awful way to do it.
But, hey, we’re here to have fun, and making mistakes is fun.
A Quick Correction
Making mistakes isn’t fun at all!
But seriously, when we added the area above the grid, our “winning” line got out of whack.
The deal is, we added our line to our “outerGroup” group component, but then we added more components to that group when initially its only purpose was to provide a place to put the line that wouldn’t screw up the tic-tac-toe grid.
So, all we have to do is wrap the board (again!) in a group, and call this one “innerGroup”, then add and remove the line to that group. It’s an easy fix.
But the outerGroup actually isn’t serving a purpose any more, because we’ve put the whole thing into a vBox. We can’t just get rid of outerGroup because it’s the handle we use to attach the button events:
NodeController.me.addHandler(outerGroup, (e) -> {
var id = ((Button) e.getTarget()).getId();
if (id != null) {
switch (id) {
And we can’t use innerGroup in there because it’s not in the NodeController.
Now, SceneBuilder has an unwrap menu item as well, so theoretically we can just eliminate the Group and rename the vbox “outerGroup”. I’ve copied over my FXML to a safe location lest this lead to disastrous consequences.
<drum roll>
Aaand, it crashed. The SceneBuilder removed the outerGroup just fine, but if you’ll recall, we’re capturing the outerGroup in our code as a Group, which it no longer is. Hence, running the app leads to disaster. Easily fixed however, by changing the definition of outerGroup:
@FXML
public VBox outerGroup;
Our line looks fine now:
I do notice that the Vbox is appearing to the left rather than the center, as an apparent result of this change. A quick debug shows it’s going in the center area of the border pane, so that’s not the problem.
It’s the Alignment property of the outerGroup VBox. Set that to CENTER and the grid will appear in the right place. The top HBox also should be set to CENTER, or it will put its own children top-left.
Not bad. I feel like we’re learning.
More State
We have a mess of state, currently, and we’re not even using it properly. Lets keep track of who wins. In our handleOnDrop code, we need to add to the gameOver code:
if (gameOver) {
System.out.println("GAME OVER! " + winner + " WINS!");
if ((winner == "X" & playerOneIsX) | (winner == "O" & !playerOneIsX)) {
playerOneWins++;
} else {
playerTwoWins++;
}
}
But this does not actually show up anywhere. We can update the labels accordingly:
if (gameOver) {
System.out.println("GAME OVER! " + winner + " WINS!");
if (winner.equals("X") && playerOneIsX) {
playerOneWins++;
p1Score.setText(String.valueOf(playerOneWins));
} else {
playerTwoWins++;
p2Score.setText(String.valueOf(playerTwoWins));
}
But one quickly begins to see the value of binding variables with onscreen controls. Anyway, lets check out our save game code.
If, when you click on the Save button, nothing happens, recall that our code to handle the buttons from inside the TTT Controller starts like this:
public void initialize() {
NodeController.me.addHandler(outerGroup, (e) -> {
var id = ((Button) e.getTarget()).getId();
if (id != null) {
switch (id) {
case "new":
You may have forgotten, as I did, to set the IDs for the “Save” and “Load” buttons back when we were setting the ID for the “New” button. You may also have forgotten, because it’s been a while, that we’re looking at the actual ID field, not the fx:id field, which is used to populate fields in our classes.
ID is placed helpfully in the middle of the properties tab of the inspector:
So make sure to set “load” and “save” now.
Having done that, we do indeed get the file dialog box and a save file is created:
This is more or less what I expected to see. The array is being printed out as a JavaArray of string, no actual values, which was the big question. Let’s put in some player names and have one guy win nine games to the other guy’s one win.
So this confirms things: The text is being written out in binary, including the 9 wins appearing as a tab. SOH is the character value of 1, and NUL is the character value 0, for ties which we haven’t figured out yet.
Now, here’s the thing: There aren’t any NULs after “Version 1”, “Joe” or “Fred”, which tells me when we write out a string, we’re just writing out the characters. So we can read the binary numeric data back just fine, because they’re a fixed size. We could output the strings as their maximum length (I think we have it set to 32) but I’ll be honest — I’ve been down this road before, probably before you were born, and it’s no fun at all.
If there’s an advantage to being “sloppy” when we’re starting out learning things, it’s because we can do things quickly and don’t care about the long term.
If you start getting into elaborate schemes to preserve your sloppiness, it’s gone from being a helpful policy while learning to a fetish. So let’s table this for now and come back fresh, with a plan to clean up our mess, and take an approach that’s potentially more useful for the future.