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:
- Clarity in variable and method names.
- Redundancies.
- Anything unnecessarily convoluted.
- General efficiency of this approach.
- Edge Cases- Again, everything works, but I can't help but think there's something I've not encompassed in the code.
- General improvements/feedback welcomed.
The code will follow the screenshot, with imports included to facilitate things.
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.