12
\$\begingroup\$

My attempt at this challenge., incorporating lessons from this question, which turned out into a much bigger project than I anticipated, but it works. I'll definitely be refactoring most of this in the future, maybe going the MVC route. I tried a lot of new things here and am curious which are good and which are far removed from such.

Concerns:

  1. Clarity in variable and method names.
  2. Redundancies.
  3. Anything unnecessarily convoluted.
  4. General efficiency of this approach.
  5. Edge Cases- Again, everything works, but I can't help but think there's something I've not encompassed in the code.
  6. General improvements/feedback welcomed.

The code will follow the screenshot, with imports included to facilitate things.

Game screen shot

The Winner enum

public enum Winner {
    NONE(""),
    X("-fx-color: darkred;"),
    O("-fx-color: gold"),
    TIE("-fx-color: orangered;");

    private final String style;

    Winner(String style) {
        this.style = style;
    }

    public String getStyle() {
        return style;
    }
}

The Player enum

public enum Player {
    X("-fx-text-fill: darkred;"),
    O("-fx-text-fill: gold;");

    private final String style;

    Player(String style) {
        this.style = style;
    }

    public String getStyle() {
        return style;
    }
}

The Position enum

public enum Position {
    TOP_LEFT,
    TOP_MIDDLE,
    TOP_RIGHT,
    MIDDLE_LEFT,
    MIDDLE,
    MIDDLE_RIGHT,
    BOTTOM_LEFT,
    BOTTOM_MIDDLE,
    BOTTOM_RIGHT;
}

The Tic Tac Toe Square Class

import javafx.scene.control.Button;

public class TicTacToeSquare {
    private Button button = new Button();
    private final int SQUARE_LENGTH = 70;

    TicTacToeSquare(TicTacticsGame game, TicTacToeBoard board, Position position) {
        button.setMinSize(SQUARE_LENGTH, SQUARE_LENGTH);
        button.setOnAction(e -> {
            if (button.getText().isEmpty()) {
                button.setText(game.getCurrentPlayer().toString());
                button.setStyle(game.getCurrentPlayer().getStyle());
                board.evaluateState();
                game.endTurn();
                game.board().disable();
                game.board().enable(position);
            }
        });
    }

    public Button button() {
        return button;
    }

    public boolean equivalentTo(TicTacToeSquare target) {
        return !button.getText().isEmpty() && button.getText().equals(target.button().getText());
    }

    public void reset() {
        button.setText("");
        button.setStyle("");
        button.setDisable(false);
    }
}

The Tic Tac Toe Board Class

import javafx.scene.layout.GridPane;

public class TicTacToeBoard extends GridPane {
    private final int NUMBER_OF_SQUARES = 9;
    public int boardCounter;
    private TicTacToeSquare[] board = new TicTacToeSquare[NUMBER_OF_SQUARES];
    private TicTacticsGame game;
    private boolean captured = false;
    private Winner winner = Winner.NONE;

    TicTacToeBoard(TicTacticsGame game) {
        this.game = game;

        for (int i = 0; i < board.length; i++) {
            board[i] = new TicTacToeSquare(this.game, this, Position.values()[i]);
            add(board[i].button(), i / 3, i % 3);
        }
        setStyle("-fx-border-color: cadetblue; -fx-border-width: 2; -fx-border-radius: 5");
    }

    public void evaluateState() {
        for (int horizontal = 0, vertical = 0; horizontal < NUMBER_OF_SQUARES; horizontal += 3) {
            if (checkSet(vertical, vertical + 3, vertical++ + 6) 
            ||  checkSet(horizontal, horizontal + 1, horizontal + 2)) {
                return;
            }
        }

        // Diagonal
        if(checkSet(0, 4, 8) || checkSet(2, 4, 6)) {
            return;
        }

        if (++boardCounter == NUMBER_OF_SQUARES) {
            winner = Winner.TIE;
            captured = true;
            game.evaluateBoard();
            styleBoard();
            return;
        }
    }

    private boolean checkSet(int square1, int square2, int square3) {
        if (boardCounter >= 2) {
            if (board[square1].equivalentTo(board[square2]) 
            && board[square2].equivalentTo(board[square3])) {
                if (!captured) {
                    winner = board[square1].button().getText().equals("X") ? Winner.X : Winner.O;
                    captured = true;
                    game.evaluateBoard();
                }
                styleBoard();
                return true;
            }
        }
        return false;
    }

    public boolean equivalentTo(TicTacToeBoard target) {
        return winner != Winner.NONE && (winner == target.winner() || target.winner() == Winner.TIE);
    }

    public Winner winner() {
        return winner;
    }

    private void styleBoard() {
        for (TicTacToeSquare square : board) {
            square.button().setStyle(winner.getStyle());
        }
    }

    public void disable() {
        for (int i = 0; i < board.length; i++) {
            board[i].button().setDisable(true);
        }
    }

    public void enable() {
        for (int i = 0; i < board.length; i++) {
            board[i].button().setDisable(false);
        }
    }

    public boolean isCaptured() {
        return captured;
    }

    public boolean isFilled() {
        for (TicTacToeSquare square : board) {
            if (square.button().getText().isEmpty()) {
                return false;
            }
        }
        return true;
    }

    public void reset() {
        captured = false;
        winner = Winner.NONE;
        boardCounter = 0;
        for (int i = 0; i < board.length; i++) {
            board[i].reset();
        }
    }
}

The Tic Tactics Board Class

import javafx.scene.layout.GridPane;

public class TicTacticsBoard extends GridPane {
    private final int NUMBER_OF_BOARDS = 9;
    public int boardCounter = 1;
    private TicTacToeBoard[] board = new TicTacToeBoard[NUMBER_OF_BOARDS];
    private TicTacticsGame game;

    TicTacticsBoard(TicTacticsGame game) {
        this.game = game;

        for (int i = 0; i < board.length; i++) {
            board[i] = new TicTacToeBoard(this.game);
            add(board[i], i / 3, i % 3);
        }
        setHgap(5);
        setVgap(5);
    }

    public void evaluateState() {
        for (int horizontal = 0, vertical = 0; horizontal < NUMBER_OF_BOARDS; horizontal += 3) {
            if (checkSet(vertical, vertical + 3, vertical++ + 6) 
            ||  checkSet(horizontal, horizontal + 1, horizontal + 2)) {
                return;
            }
        }
        // Diagonal
        if(checkSet(0, 4, 8) || checkSet(2, 4, 6)) {
            return;
        }

        if (++boardCounter == NUMBER_OF_BOARDS) {
            game.endPrompt("It's a tie!");
            return;
        }
    }

    private boolean checkSet(int innerBoard1, int innerBoard2, int innerBoard3) {
        if (boardCounter >= 3) {
            if (board[innerBoard1].equivalentTo(board[innerBoard2]) 
            && board[innerBoard2].equivalentTo(board[innerBoard3])) {
                game.endPrompt(game.checkWinner(board[innerBoard1].winner().toString()) + " wins!");
                return true;
            }
        }
        return false;
    }

    public void disable() {
        for (TicTacToeBoard b : board) {
            b.disable();
        }
    }

    public void enable(Position position) {
        for (int i = 0; i < Position.values().length; i++) {
            if (position == Position.values()[i]) {
                if (board[i].isCaptured()) {
                    enableAll();
                } else {
                    board[i].enable();
                }
                break;
            }
        }
    }

    public void enableAll() {
        for (TicTacToeBoard b : board) {
            b.enable();
        }
    }

    public void reset() {
        for (TicTacToeBoard b : board) {
            b.reset();
        }
    }
}

The Game Class

import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.beans.binding.Bindings;
import javafx.beans.property.IntegerProperty;
import javafx.beans.property.SimpleIntegerProperty;
import javafx.beans.property.StringProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.scene.Scene;
import javafx.scene.text.Text;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.input.KeyCode;
import javafx.scene.input.KeyCodeCombination;
import javafx.scene.input.KeyCombination;
import javafx.scene.layout.HBox;
import javafx.scene.layout.VBox;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.GridPane;
import javafx.stage.Stage;

public class TicTacticsGame extends BorderPane {
    private StringProperty xPlayer = new SimpleStringProperty("X player");
    private StringProperty oPlayer = new SimpleStringProperty("O player");
    private IntegerProperty xScore = new SimpleIntegerProperty(0);
    private IntegerProperty oScore = new SimpleIntegerProperty(0);
    private IntegerProperty tieScore = new SimpleIntegerProperty(0);
    private boolean scoreDisplayed;
    private Player currentPlayer = Player.X;
    private TicTacticsBoard board;

    TicTacticsGame(Stage stage) {
        board = new TicTacticsBoard(this);
        setTop(generateMenuBar(stage));
        setCenter(board);
    }

    private MenuBar generateMenuBar(Stage stage) {
        MenuItem newGameItem = new MenuItem("_New Game");
        newGameItem.setAccelerator(new KeyCodeCombination(KeyCode.N, KeyCombination.SHORTCUT_DOWN));
        newGameItem.setOnAction(e -> newGame());

        MenuItem exitItem = new MenuItem("E_xit");
        exitItem.setOnAction(e -> Platform.exit());

        Menu gameMenu = new Menu("_Game");
        gameMenu.getItems().addAll(newGameItem, exitItem);

        MenuItem addItem = new MenuItem("_Add player name(s)");
        addItem.setAccelerator(new KeyCodeCombination(KeyCode.A, KeyCombination.SHORTCUT_DOWN));
        addItem.setOnAction(e -> addName(stage));

        Text xText = new Text();
        xText.textProperty().bind(
            Bindings.concat(xPlayer).concat(" wins: ").concat(xScore.asString())
        );

        Text oText = new Text();
        oText.textProperty().bind(
            Bindings.concat(oPlayer).concat(" wins: ").concat(oScore.asString())
        );

        Text tieText = new Text();
        tieText.textProperty().bind(
            Bindings.concat("Ties: ").concat(tieScore.asString())
        );

        VBox scoreLayout = new VBox(5);
        scoreLayout.getChildren().addAll(xText, oText, tieText);
        scoreLayout.setPadding(new Insets(5));
        scoreLayout.setAlignment(Pos.CENTER);

        MenuItem trackItem = new MenuItem("_Toggle score display");
        trackItem.setAccelerator(new KeyCodeCombination(KeyCode.T, KeyCombination.SHORTCUT_DOWN));
        trackItem.setOnAction(e -> {
            if (!scoreDisplayed) {
                setRight(scoreLayout);
                scoreDisplayed = true;
                stage.sizeToScene();
            } else {
                setRight(null);
                scoreDisplayed = false;
                stage.sizeToScene();
            }
        });

        MenuItem resetItem = new MenuItem("_Reset score");
        resetItem.setAccelerator(new KeyCodeCombination(KeyCode.R, KeyCombination.SHORTCUT_DOWN));
        resetItem.setOnAction( e -> {
            xScore.set(0);
            oScore.set(0);
            tieScore.set(0);
        });

        Menu scoreMenu = new Menu("_Score");
        scoreMenu.getItems().addAll(
            addItem,
            trackItem,
            resetItem
        );

        MenuItem howToItem = new MenuItem("How to _play");
        howToItem.setAccelerator(new KeyCodeCombination(KeyCode.H, KeyCombination.SHORTCUT_DOWN));
        howToItem.setOnAction(e -> showGameRules());

        Menu helpMenu = new Menu("_Help");
        helpMenu.getItems().add(howToItem);

        activateMnemonics(
            gameMenu,
            newGameItem,
            exitItem,
            scoreMenu,
            addItem,
            trackItem,
            resetItem,
            helpMenu,
            howToItem
        );

        MenuBar menuBar = new MenuBar();
        menuBar.getMenus().addAll(gameMenu, scoreMenu, helpMenu);
        return menuBar;
    }

    public void endPrompt(String message) {
        board.disable();

        if (message.equals("It's a tie!")) {
            tieScore.setValue(tieScore.getValue() + 1);
        }

        Stage stage = new Stage();
        Label label = new Label(message);
        label.setStyle("-fx-font-weight: bold;");

        final int BUTTON_WIDTH = 80;

        Button reset = new Button("New Round");
        reset.setMinWidth(BUTTON_WIDTH);
        reset.setOnAction(e -> {
            stage.close();
            newRound();
        });
        reset.setDefaultButton(true);

        Button quit = new Button("Quit");
        quit.setMinWidth(BUTTON_WIDTH);
        quit.setOnAction(e -> Platform.exit());

        HBox gameLayout = new HBox(5);
        gameLayout.getChildren().addAll(reset, quit);
        gameLayout.setAlignment(Pos.CENTER);

        VBox layout = new VBox(5);
        layout.getChildren().addAll(label, gameLayout);
        layout.setAlignment(Pos.CENTER);

        stage.setScene(new Scene(layout, 175 + new Text(message).getLayoutBounds().getWidth(), 75));
        stage.sizeToScene();
        stage.setTitle("Game Over");
        stage.show();
    }

    public Player getCurrentPlayer() {
        return currentPlayer;
    }

    private void activateMnemonics(MenuItem... items) {
        for (MenuItem item : items) {
            item.setMnemonicParsing(true);
        }
    }

    private void newGame() {
        board.boardCounter = 0;
        currentPlayer = Player.X;
        board.reset();
    }

    private void newRound() {
        board.boardCounter = 0;
        board.reset();
    }

    private void addName(Stage primaryStage) {
        Stage stage = new Stage();

        Label xName = new Label("Enter X player: ");
        GridPane.setConstraints(xName, 0, 0);
        TextField xPlayerField = new TextField();
        GridPane.setConstraints(xPlayerField, 1, 0);

        Label oName = new Label("Enter O player: ");
        GridPane.setConstraints(oName, 0, 1);
        TextField oPlayerField = new TextField();
        GridPane.setConstraints(oPlayerField, 1, 1);

        Button submit = new Button("Submit");
        submit.setOnAction(e -> {
            String xString = xPlayerField.getText();
            String oString = oPlayerField.getText();
            if (!xString.replaceAll("[^a-zA-Z]", "").isEmpty()) {
                xPlayer.setValue(xString);
            }
            if (!oString.replaceAll("[^a-zA-Z]", "").isEmpty()) {
                oPlayer.setValue(oString);
            }
            primaryStage.sizeToScene();
            stage.close();
        });
        submit.setDefaultButton(true);
        GridPane.setConstraints(submit, 0, 2);

        GridPane layout = new GridPane();
        layout.getChildren().addAll(
            xName,
            xPlayerField,
            oName,
            oPlayerField,
            submit
        );

        stage.setScene(new Scene(layout));
        stage.setTitle("Set name(s): ");
        stage.sizeToScene();
        stage.show();
    }

    public String checkWinner(String winner) {
        if (winner.equals("X")) {
            xScore.setValue(xScore.getValue() + 1);
            return xPlayer.getValue();
        } else {
            oScore.setValue(oScore.getValue() + 1);
            return oPlayer.getValue();
        }
    }

    public void showGameRules() {
        Stage stage = new Stage();
        TextArea area = new TextArea();
        area.setEditable(false);
        area.setText("1. Each turn, you mark one of the small squares."
            + "\n2. When you get three in a row on a small board, you’ve won that board."
            + "\n3. To win the game, you need to win three small boards in a row."
            + "\n4. Tied boards may count for either X or O."
            + "\n\nYou don't get to pick the board you play on:"
            + "\n*Whichever square your opponent picks corresponds to the board you must play in"
            + "\n*If your opponent sends you to an already won board, you may play anywhere");
        stage.setScene(new Scene(area));
        stage.setTitle("Rules");
        stage.show();
    }

    public void endTurn() {
        currentPlayer = currentPlayer == Player.X ? Player.O : Player.X;
    }

    public void evaluateBoard() {
        board.evaluateState();
    }

    public TicTacticsBoard board() {
        return board;
    }
}

Class to run it

import javafx.application.Application;
import javafx.scene.Scene;
import javafx.stage.Stage;

public class TicTactics extends Application {
    public static void main(String[] args) {
        launch(args);
    }

    @Override
    public void start(Stage stage) {
        stage.setTitle("Tic Tactics by Legato");
        stage.setScene(new Scene(new TicTacticsGame(stage)));
        stage.show();
    }
}

Feel free to try it. Here's a jar for those that can't run executables. and the Github repo where it will be maintained.

\$\endgroup\$
3
\$\begingroup\$

final Keyword

The Game, Tic Tac Toe Board, Tic Tac Toe Board, all have fields which are assigned but never reassigned,

private StringProperty xPlayer = new SimpleStringProperty("X player");
private StringProperty oPlayer = new SimpleStringProperty("O player");
private TicTacticsGame game;
private Button button = new Button();

They could all use the final keyword.

Generally

The code is fairly easy to read, you follow most conventions. Overall a pleasant surprise to look at.

\$\endgroup\$
  • 2
    \$\begingroup\$ What exactly do you mean by separating initialization and launch here? \$\endgroup\$ – Simon Forsberg Aug 19 '15 at 18:59
  • \$\begingroup\$ Generally speaking (from my experience and feedback gathered here) you want to different between initialization and launch, Meaning you want to separate launch, start and so on; from main. Just like thread is handled. \$\endgroup\$ – Emz Aug 22 '15 at 2:31
  • \$\begingroup\$ In that case I have to disagree with you, this is how it is typically done in JavaFX applications, I see no problem with that. \$\endgroup\$ – Simon Forsberg Aug 22 '15 at 10:39
  • \$\begingroup\$ Late comment; however it seems that you are right. I stand corrected. I've now updated it to reflect that. \$\endgroup\$ – Emz Aug 25 '15 at 20:25
0
\$\begingroup\$

Streamline logic

There are several cases where some logic is unnecessarily repeated and /or could be simplified. e.g. In the following,

trackItem.setOnAction(e -> {
     if (!scoreDisplayed) {
            setRight(scoreLayout);
            scoreDisplayed = true;
            stage.sizeToScene();
     } else {
            setRight(null);
            scoreDisplayed = false;
            stage.sizeToScene();
     }
});

In both cases, stage.SsizeToScene() is invoked it should be independent of control flow. Similarly, notice that setting scoreDisplayed to true in one case and false in the other is really just inverting the current value. The only thing actually dependent is the setRight method and that is straightforward enough for the conditional operator.

trackItem.setOnAction(e -> {
    setRight(scoreDisplayed ? null : scoreLayout);
    scoreDisplayed = !scoreDisplayed;
    stage.sizeToScene();
 });

There are also cases where some objects are closely coupled they may merit clustering into another. For instance, take the proceeding:

private StringProperty xPlayer = new SimpleStringProperty("X player");
private IntegerProperty xScore = new SimpleIntegerProperty(0);

Text xText = new Text();
        xText.textProperty().bind(
            Bindings.concat(xPlayer).concat(" wins: ").concat(xScore.asString())
        );

This is all to represent the idea of a player, their score, and what text displays. This exact pattern is repeated for oPlayer, oScore and oText While largely a design decision, it would further simplify the code if these were clustered together. Rather than a Player enum a Player class could be created to house this logic, renaming what is now Player to PlayerStyle which could also be included in the new Player class.

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.