FX My Life XII: Super Serial

Blake
16 min readMay 25, 2021

Last time, we left things hanging a bit because we wanted to save our game data and it was becoming too complicated — or as Rich Hickey put it in his seminal talk “Simple Made Easy”, complected. To “complect” something is to mix concerns, so that we have game logic and game presentation mixed up in the same object.

I did this deliberately, of course — no, really, as much as I make mistakes and pretend I did them on purpose, this really was intentional on a number of levels. One of those reasons is: if you know you have a good working concept, it’ll prove itself out over your old way of working very quickly, except on the most trivial of examples.

Tic-tac-toe is about as trivial an example as I could think of, and the strain of mashing up game state with the view object has been showing around the edges all along. Now, let’s examine how it completely falls apart.

The Serializable Interface

I remember the first time I encountered insta-serialization outside of Smalltalk. It was in Python, and you could just “pickle” the entirety of…everything…and it would all just save and load with ease. (Python tends to be fun like that.) Up till then I had generally written my own serialization code for objects which involves reading and writing the specific object fields to disk.

Java has its own serializer, going back at least to 2011, and it is the Serializable Interface. We can literally just add that our controller class implements java.io.Serializable and our controller will save to disk:

public class TttController implements java.io.Serializable

Our save code will look like this:

case "save":
FileChooser fileChooser = new FileChooser();
FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter("tic-tac-toe files (*.ttt)", "*.ttt");
fileChooser.getExtensionFilters().add(extFilter);
file = fileChooser.showSaveDialog(Main.me.stage);
if (file != null) {
try {
FileOutputStream fileOut = new FileOutputStream(file);
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(this);
out.close();
fileOut.close();
} catch (IOException i) {
i.printStackTrace();
}
}
break;

(If you sense I’m setting you up, you are sensing correctly.) Now, when we read this back, what do we use?

Our load code might look something like this:

case "load":
file = getFilename(true);
if (file != null)
try {
FileInputStream fileIn = new FileInputStream(file);
ObjectInputStream in = new ObjectInputStream(fileIn);
var t = (TttController) in.readObject();
in.close();
fileIn.close();

this.state = t.state;
this.winner = t.winner;
this.gameOver = t.gameOver;
} catch (IOException | ClassNotFoundException i) {
i.printStackTrace();
return;
}
break;

Java doesn’t have a become: like Smalltalk (and that’s a controversial method in that community, sometimes), and I don’t think it even has an Assign, like Delphi. It has a clone, but that’s of no value, since we basically want to either replace the current object instance with the loaded one (a tricky thing from inside the object itself), or assign all its values over.

But, heck, what values do we need, rally? The state of the board, whether there’s a winner, and whether the game is over. Hmmm, we also need the wins for both players and, more significantly, we need things like the game state to match up with the visuals.

Like, what do we do with the strike-through for a victory condition? Or the Xs and the Os that might be on the board. They have to match the game state.

Well, this is that issue that we were talking about: Instead of keeping a game state and having the front end reflect that, we tended to change the game state and the board at the same time.

Simple case, remember?

Though not so simple now in this very trivial problem of persisting the state.

And there’s more: If you actually run this code, you’re going to find a problem with the serializable interface. Namely, imageviews aren’t serializable. Probably a lot of graphical components aren’t. Hell, when we write out what we can, it turns out that we end up writing SIX kilobytes.

6KB ain’t a lot, but it seems a bit excessive for something that would be swimming in 60 bytes.

So, what if we put the game state into a class of its own, did all the changes to that class, and had the GUI update itself accordingly, while only saving the game state and not all the graphic stuff with it.

Well, Duh

This is one of those ideas I had as a young man, maybe even a kid, but it was never really feasible for any serious game development until recently. If you’ve ever seen a game like Nethack:

The castle leads to Gehennom. Watch out, Medusa is waiting for you.

The reason games looked like this and had many of the mechanics they have (some carried forward to this day!) is because everything was stored in a two-dimensional array of a single byte. You drew your map from this and you worked all your game logic over it.

Having a separate representation meant doubling your memory usage, having to transfer the map to the screen somehow (which presented the possibility of being out of sync), and ending up with some sort of game state you couldn’t display anyway.

But we can afford those kinds of costs now, especially for a game of tic-tac-toe. So what should be in the game state? Well, if we look at our (hilariously sloppy) TttController definition, we see:

@FXML
public GridPane xobin;
@FXML
public transient ImageView X;
@FXML
public transient ImageView O;
@FXML
public GridPane board;
@FXML
public VBox outerGroup;
@FXML
public Group innerGroup;
@FXML
public TextField player1;
@FXML
public TextField player2;
@FXML
public Label p1label;
@FXML
public Label p2Label;
@FXML
public Label p1Score;
@FXML
public Label p2Score;
@FXML
public ImageView XoverO;

private String[][] state = new String[3][3];
private String winner;
private boolean gameOver = false;
private boolean playerOneIsX = true;
private Line line = new Line();
private int playerOneWins = 0;
private int playerTwoWins = 0;
private int Ties = 0;

OK, maybe we’re not so awful: That’s almost a clean break, with all the @FXML controls staying in the controller and the private variables below almost all going to game state. The exception would be the Line.

Our “new” code isn’t so clean.

state = new String[3][3];
winner = "";
innerGroup.getChildren().remove(line);
gameOver = false;
for (int i = board.getChildren().size() - 1; i > 0; i--) board.getChildren().remove(i);

We’re both resetting game state and removing objects from the board. What we really want to do is this:

case "new":
game.newGame();
break;
case "load":

Calling “newGame” on our game object should reset it, and the board should draw itself accordingly. OK, well, what’s newGame look like?

Well, the whole TicTacToe class looks like:

package fxgames;

import java.io.Serializable;

public class TicTacToe implements Serializable {
private String[][] state = new String[3][3];
private String winner;
private boolean gameOver = false;
private boolean playerOneIsX = true;
private int playerOneWins = 0;
private int playerTwoWins = 0;
private int Ties = 0;

public TicTacToe() {
newGame();
}

public void newGame() {
state = new String[3][3];
winner = "";
gameOver = false;
}
}

It’s interesting that most of our variables are things we don’t want to change on a new game. We don’t want to switch who’s “X” or to reset the total wins. We will need a function to do those things, however, or the only way new players will be able to play (and have their scores counted properly) is to quit the program and re-run it. Tacky.

The player names should actually be part of the object as well. We didn’t even have separate variables for that previously because we were just using the graphical controls to store them.

I also don’t like the “gameOver” and “winner” variables. Let’s just have a “winner” variable. We could probably do something fancy here, but let’s make it a string that will be empty when there’s no winner, “X” or “O” if there’s a winner (and therefore the game is over) and…oh, let’s have a tie be “-”.

We should probably put in the “tie” logic, come to think of it. And we want to move all the game logic from the interface over into this new class.

Man, all this re-factoring makes me wish we had tests.

Some of the stuff we did will transfer over easily to the non-visual class. Like our silly CheckVictoryCondition will work fine:

public void checkVictoryCondition() {
check(0, 0, 0, 1);
check(1, 0, 0, 1);
check(2, 0, 0, 1);
check(0, 0, 1, 0);
check(0, 1, 1, 0);
check(0, 2, 1, 0);
check(0, 0, 1, 1);
check(2, 0, -1, 1);
}

But we’re definitely going to need to make some changes over in the check routine itself, since it has two parts. But at least we divided that routine so that the logic was at the top and the graphical changes came later. The game logic will look something like this now:

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];
}

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

public void checkVictoryCondition() {
check(0, 0, 0, 1);
check(1, 0, 0, 1);
check(2, 0, 0, 1);
check(0, 0, 1, 0);
check(0, 1, 1, 0);
check(0, 2, 1, 0);
check(0, 0, 1, 1);
check(2, 0, -1, 1);
}

Now, we have a bunch of stuff on the GUI side that need to look into the class fields, so we can actually make our fields public and just maintain that. Of all the sloppy things we’ve done to date, this one is the cringiest for me, but let’s take a look at how it would play out.

The game object would look like this:

package fxgames;

import java.io.Serializable;

public class TicTacToe implements Serializable {
public String[][] state = new String[3][3];
public String winner;
public boolean playerOneIsX;
public String playerOneName;
public int playerOneWins;
public String playerTwoName;
public int playerTwoWins;
public int ties;

public int vx;
public int vy;
public int vxd;
public int vyd;

public TicTacToe() {
resetGame();
}

public void newGame() {
state = new String[3][3];
winner = "";
}

public void resetGame() {
newGame();
playerOneIsX = true;
playerOneName = "";
playerOneWins = 0;
playerTwoName = "";
playerTwoWins = 0;
ties = 0;
}

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];
}

public String check(int x, int y, int xd, int yd) {
if (winner == "") {
String s = bstr(x, y, xd, yd);
vx = x;
vy = y;
vxd = xd;
vyd = yd;
if (s.equals("OOO") || s.equals("XXX")) {
winner = s.substring(0, 1);
}
}
return winner;
}

public void checkVictoryCondition() {
check(0, 0, 0, 1);
check(1, 0, 0, 1);
check(2, 0, 0, 1);
check(0, 0, 1, 0);
check(0, 1, 1, 0);
check(0, 2, 1, 0);
check(0, 0, 1, 1);
check(2, 0, -1, 1);
}

}

This is okay. The GUI class now looks like:

package fxgames;

import javafx.event.ActionEvent;
import javafx.fxml.FXML;
import javafx.scene.Group;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextField;
import javafx.scene.image.ImageView;
import javafx.scene.input.*;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.VBox;
import javafx.scene.shape.Line;
import javafx.stage.FileChooser;

import java.io.*;

public class TttController {

@FXML
public GridPane xobin;
@FXML
public transient ImageView X;
@FXML
public transient ImageView O;
@FXML
public GridPane board;
@FXML
public VBox outerGroup;
@FXML
public Group innerGroup;
@FXML
public TextField player1;
@FXML
public TextField player2;
@FXML
public Label p1label;
@FXML
public Label p2Label;
@FXML
public Label p1Score;
@FXML
public Label p2Score;
@FXML
public ImageView XoverO;

private Line line = new Line();
private TicTacToe game = new TicTacToe();

@FXML
public void initialize() {
NodeController.me.addHandler(outerGroup, (e) -> {
var id = ((Button) e.getTarget()).getId();
File file;
if (id != null) {
switch (id) {
case "new":
game.newGame();
break;
case "load":
file = getFilename(true);
if (file != null)
try {
FileInputStream fileIn = new FileInputStream(file);
ObjectInputStream in = new ObjectInputStream(fileIn);
game = (TicTacToe) in.readObject();
in.close();
fileIn.close();
} catch (IOException | ClassNotFoundException i) {
i.printStackTrace();
return;
}
break;
case "save":
FileChooser fileChooser = new FileChooser();
FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter("tic-tac-toe files (*.ttt)", "*.ttt");
fileChooser.getExtensionFilters().add(extFilter);
file = fileChooser.showSaveDialog(Main.me.stage);
if (file != null) {
try {
FileOutputStream fileOut = new FileOutputStream(file);
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(game);
out.close();
fileOut.close();
} catch (IOException i) {
i.printStackTrace();
}
}
break;
default:
System.out.println("what?");
}
}
});
}

public File getFilename(boolean mustExist) {
FileChooser fileChooser = new FileChooser();
FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter("tic-tac-toe files (*.ttt)", "*.ttt");
fileChooser.getExtensionFilters().add(extFilter);
if (mustExist) return fileChooser.showOpenDialog(Main.me.stage);
else return fileChooser.showSaveDialog(Main.me.stage);
}

public void drawVictorySlash() {
if (game.check(game.vx, game.vy, game.vxd, game.vyd) != "") {
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;

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

public void handleOnDragDetected(MouseEvent event) {
Dragboard db = X.startDragAndDrop(TransferMode.ANY);

ClipboardContent content = new ClipboardContent();
content.putString(((ImageView) event.getSource()).getId());
db.setContent(content);
}

public int getCoord(double width, double coord, int numberOfSections) {
long dim = Math.round(width / numberOfSections);
long border = dim;
int val = 0;
while (coord > border) {
border += dim;
val++;
}
return val;
}

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

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

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

ImageView xo = new ImageView();

if (event.getDragboard().getString().equals("X"))
xo.setImage(X.getImage());
else xo.setImage(O.getImage());

xo.setFitWidth(Math.round(board.getWidth() / 3));
xo.setFitHeight(Math.round(board.getHeight() / 3));
GridPane.setRowIndex(xo, column);
GridPane.setColumnIndex(xo, row);
board.getChildren().addAll(xo);

game.state[column][row] = event.getDragboard().getString();
game.checkVictoryCondition();

if (game.winner != "") {
System.out.println("GAME OVER! " + game.winner + " WINS!");
drawVictorySlash();
if (game.winner.equals("X") && game.playerOneIsX) {
game.playerOneWins++;
p1Score.setText(String.valueOf(game.playerOneWins));
} else {
game.playerTwoWins++;
p2Score.setText(String.valueOf(game.playerTwoWins));
}
}
}

public boolean handler(ActionEvent e) {
System.out.println(e);
return false;
}

public void nameChange(KeyEvent e) {
Label l = (e.getTarget() == player1) ? p1label : p2Label;
l.setText(((TextField) e.getTarget()).getText());
}

public void togglePlayerX() {
game.playerOneIsX = !game.playerOneIsX;

if (game.playerOneIsX) {
XoverO.setRotate(0);
} else {
XoverO.setRotate(180);
}
}
}

On the plus side, the code looks quite a bit simpler now that we selected out a lot of the non-GUI stuff. (You can see that I’ve partially transitioned over to a function dedicated to popping up the load/save dialog. That may ultimately end up in a different location entirely.) And while I don’t think that much is necessarily gained by making properties out of things, solving problems by exposing object fields to direct manipulation is probably not the way to go.

Let’s see how this is simplified by this change: Rather than having the GUI maintain its state and change it incrementally, let’s just have the GUI draw itself when the game state changes. Here’s what they call a “naïve implementation” of drawing the grid:

public void draw() {
innerGroup.getChildren().remove(line);
for (int i = board.getChildren().size() - 1; i > 0; i--) board.getChildren().remove(i);

for (var i = 0; i < 3; i++) {
for (var j = 0; j < 3; j++)
if (game.state[i][j] != null) {
ImageView xo = new ImageView();

if (game.state[i][j].equals("X"))
xo.setImage(X.getImage());
else if (game.state[i][j].equals("O"))
xo.setImage(O.getImage());
xo.setFitWidth(Math.round(board.getWidth() / 3));
xo.setFitHeight(Math.round(board.getHeight() / 3));
GridPane.setRowIndex(xo, i);
GridPane.setColumnIndex(xo, j);
board.getChildren().addAll(xo);
}
}
drawVictorySlash();
}

“Naïve” means “I don’t know what I’m doing, but I suspect I’m doing it wrong.” In this case, we wipe out the board every time, remove the slash, then add the Xs and Os back in where they should go, and then add the slash back in.

I mean, just for starters, we could skip the deletion part and change each spot one-at-a-time, since we’re pretty close to doing that already. But let’s stay innocent for a while longer.

Now, where do we call this? Any place the board’s state changes: So when handling the drop and when starting a new game. The new game code is easy enough.

public void initialize() {
NodeController.me.addHandler(outerGroup, (e) -> {
var id = ((Button) e.getTarget()).getId();
File file;
if (id != null) {
switch (id) {
case "new":
game.newGame();
draw(); //<--Easy
break;
case "load":

Meanwhile, handleOnDrop looks like:

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

game.state[column][row] = event.getDragboard().getString();
game.checkVictoryCondition();

draw();

if (game.winner != "") {
System.out.println("GAME OVER! " + game.winner + " WINS!");

if (game.winner.equals("X") && game.playerOneIsX) {
game.playerOneWins++;
p1Score.setText(String.valueOf(game.playerOneWins));
} else {
game.playerTwoWins++;
p2Score.setText(String.valueOf(game.playerTwoWins));
}
}
}

Well, that’s better than it was but look at all the game state! We should just be able to call a function to change the game state, and that should do most of the rest of this stuff. So, let’s do that. The above code should evolve to this:

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

game.addPiece(event.getDragboard().getString(), column, row);
draw();
}

And we should add an addPiece method to TicTacToe:

public void checkGameState() {
checkVictoryCondition();
if (winner != "") {
if (winner.equals("X") && playerOneIsX) {
playerOneWins++;
} else {
playerTwoWins++;
}
}
}

public void addPiece(String piece, int x, int y) {
state[y][x] = piece;
checkGameState();
}

Now, a few comments. addPiece, checkGameState and even checkVictoryCondition could all be in one method, because the only thing that can change the game state is adding a piece to the board (except starting a new game, but that doesn’t require chcking the game state, either). I’m not a fan of splitting methods up if they don’t have any applicability elsewhere — it forces maintainers to go through function calls needlessly — but it doesn’t seem too egregious here, and even is arguably a bit clearer to read.

Another point is that I’ve made these functions public. I make functions public unless I have a damn good reason not to. From the dawn of time (well, after Smalltalk-80), object-oriented libraries have stymied me with private methods. (Actually, this carries forward into non-OO languages Clojure, where vital functions may be marked as private.)

These do no harm being public that I can see, so public they are. The defense for a lot of libraries is that they want to be maintainable into the future and private methods mask implementation details that might change. Meh. In any event — and in most real-world cases — we’re not building a general purpose library, so we can make life easier on ourselves by not hiding things capriciously.

Anyway, we removed an intimate link to the TicTacToe class’s fields, and that’s good, but we’ll need to update our draw method:

                 board.getChildren().addAll(xo);
}
}
drawVictorySlash();

p1Score.setText(String.valueOf(game.playerOneWins));
p2Score.setText(String.valueOf(game.playerTwoWins));
}

Otherwise our scores won’t update.

OK, how else can we de-couple? Well, there’s togglePlayerX:

public void togglePlayerX() {
game.playerOneIsX = !game.playerOneIsX;

if (game.playerOneIsX) {
XoverO.setRotate(0);
} else {
XoverO.setRotate(180);
}
}

That can become:

public void togglePlayerX() {
game.togglePlayerX();
draw();
}

where TicTacToe has a method:

public void togglePlayerX() {
playerOneIsX = !playerOneIsX;
}

And we update our draw method:

. . .
p2Score.setText(String.valueOf(game.playerTwoWins));
if (game.playerOneIsX) {
XoverO.setRotate(0);
} else {
XoverO.setRotate(180);
}

That leaves us basically with drawVictorySlash, which seems kind of weird. It starts with:

public void drawVictorySlash() {
if (game.check(game.vx, game.vy, game.vxd, game.vyd) != "") {

Why are we passing in the coordinates of the victory (and the x and y delta) to game when game knows all this stuff? I think that’s vestigial — from shuffling our code around. No longer necessary:

public void drawVictorySlash() {
if (game.winner != "") {
var bw = board.getWidth();

Now, the rest of it is references to vx, vy and vxd, vyd, which are the variables that contain the starting coordinate of the victory and the delta. My preference would be to have a function return a record:

vc = game.getVictoryConditions();
if (vc.xd == 1 && vc.yd == 1) {

Where vc would be a record containing x, y, xd and yd fields. But Java doesn’t let us have records (objects without methods) and it seems like overkill to build a whole class. I could have it return an array, and define constants, like:

vc = game.getVictoryConditions();
if (vc[XD] == 1 && vc[YD] == 1) {

I don’t think we get that much out of either approach frankly. We’ll come back to it. Let’s, instead, wrap up with hiding our TicTacToe’s internal values where we can.

private String[][] state = new String[3][3];

If we do this, we’ll see (in IntelliJ) that we are accessing the state field directly in a few places. Well, yeah, we don’t have any way to check what’s on the board otherwise. So we’ll make a method:

public String get(int x, int y) {
return state[y][x];
}

I think, for a simple class with obvious traits, a simple method name (“get”) works best. But it’s a fine line, so be considerate to your future maintainers who may, after all, be you.

It mostly requires changes in the draw() method:

public void draw() {
. . .
for (var i = 0; i < 3; i++) {
for (var j = 0; j < 3; j++)
if (game.get(i, j) != null) {
ImageView xo = new ImageView();

if (game.get(i, j).equals("X"))
xo.setImage(X.getImage());
else if (game.get(i, j).equals("O"))
xo.setImage(O.getImage());
. . .

But there’s also a check in handleOnDragOver:

public void handleOnDragOver(DragEvent event) {
int row = getCoord(board.getWidth(), event.getX(), board.getRowCount());
int column = 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();
}

And somehow my dyslexia (NOTE: I do not have dyslexia) has kicked in, and I’ve reveresed the bstr. This is how it should be:

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

To wrap it up: We still have some encapsulation to do, and now that we have an object that controls the game state, we can do things like refuse to let “X” go on “O”s turn — oh, and work out the tie logic, which we haven’t done yet.

Similarly, I feel like it’s going to be easier to manage further graphical effects with the code better organized. Oh, and our whole point here was what, again?

We wanted to be able to save and load games…uh…“for free”. Can we?

Turns out we can! Our loading and saving code now look like:

case "load":
file = getFilename(true);
if (file != null)
try {
FileInputStream fileIn = new FileInputStream(file);
ObjectInputStream in = new ObjectInputStream(fileIn);
game = (TicTacToe) in.readObject();
in.close();
fileIn.close();
draw();
} catch (IOException | ClassNotFoundException i) {
i.printStackTrace();
return;
}
break;
case "save":
file = getFilename(false);
if (file != null) {
try {
FileOutputStream fileOut = new FileOutputStream(file);
ObjectOutputStream out = new ObjectOutputStream(fileOut);
out.writeObject(game);
out.close();
fileOut.close();
} catch (IOException i) {
i.printStackTrace();
}
}
break;

And that’s all it takes: The save automatically writes out all the TicTacToe class fields while the load reads it back in. The key element in the load section, of course, is the call to draw(). That’s the only way we can see what we’ve done.

--

--

Blake
0 Followers

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