Search code examples
javajavafxtic-tac-toe

JavaFX how could I check for winning in this occasion?


I'll try to explain my problem as best as I can. I am always happy to find patient people that can help me improve and get unstuck. Basically I am trying to build a javafx tic-tac-toe game since I feel that even from simple things like this I can learn.

So I have a main App.java file that starts the program and has some methods to correctly set the fxml file I want to use and the changing of it. This so that if in the future I'll get to the point in which I would like to add a profile for the player or the possibility to play either solo vs AI or two players locally, I can easily create another fxml file that handles the other scenes.

import javafx.application.Application;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.stage.Stage;

import java.io.IOException;

/**
 * JavaFX App
 */
public class App extends Application {

    private static Scene scene;

    @Override
    public void start(Stage stage) throws IOException {
        scene = new Scene(loadFXML("primary"));
        stage.setScene(scene);
        stage.show();
    }

    static void setRoot(String fxml) throws IOException {
        scene.setRoot(loadFXML(fxml));
    }

    private static Parent loadFXML(String fxml) throws IOException {
        FXMLLoader fxmlLoader = new FXMLLoader(App.class.getResource(fxml + ".fxml"));
        return fxmlLoader.load();
    }

    public static void main(String[] args) {
        launch();
    }

}

Then I have a PrimaryController.java file that basically wanted to use to populate the scene with Nodes. the structure is an AnchorPane with a GridPane inside and I populate the GridPane via code by adding to it MyPanes elements that are StackPanes a bit modified. here the Controller:

import java.net.URL;
import java.util.ResourceBundle;
import javafx.fxml.FXML;
import javafx.fxml.Initializable;
import javafx.scene.layout.GridPane;

public class PrimaryController implements Initializable {

    @FXML
    public GridPane grid;


    @Override
    public void initialize(URL location, ResourceBundle resources) {

        for (int i = 0; i<3; i++){
            for (int j = 0; j <3;j++){
                MyTiles tile = new MyTiles();
                grid.add(tile,j,i);
            }
        }

    }

}

and here the MyTile class

import javafx.geometry.Pos;
import javafx.scene.layout.StackPane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;
import javafx.scene.text.Font;
import javafx.scene.text.Text;



public class MyTiles extends StackPane {

    private static boolean turnOne = true;
    private Text text = new Text(" ");


    public MyTiles(){
        setPrefSize(200,200);
        Rectangle rectangle = new Rectangle(200,200);
        rectangle.setFill(null);
        rectangle.setStroke(Color.BLACK);
        text.setFont(Font.font(72));
        setAlignment(Pos.CENTER);

        getChildren().addAll(rectangle,text);

        setOnMouseClicked(event -> {
            if (text.getText().equals(" ")){
                if (turnOne){
                    drawX(text);
                    turnOne = false;
                }else if (!turnOne){
                    drawY(text);
                    turnOne = true;
                }
            }else{
                return;
            }

        });

    }


    public String getTheText(){
        return text.getText();
    }

    private void drawX(Text text){
        text.setStroke(Color.BLUE);
        text.setFill(Color.LIGHTBLUE);
        text.setText("x");
    }
    private void drawY(Text text){
        text.setStroke(Color.RED);
        text.setFill(Color.PINK);
        text.setText("o");
    }
}

Now to the question: Do you know a way in which I could check for winning? I mean I know the logic behind when a player wins but in my head I have to: - access all the MyTiles objects inside the GridPane - check what their text is - check if there is a winning combination.

I have no clue on how to do this, could somebody give me some tips, suggestions, examples?

Maybe the strategy of doing things in separate files was not the best, but I wanted to try if there was a way not to put everything in the main App.java file.

Any help is highly appreciated. I hope the question was clear enough! Thanks again!


Solution

  • You should factor the state of the game into separate classes that are independent of the user interfaces itself. (In UI development jargon, this is called a "Model" and is part of a Model-View-Controller architecture). JavaFX provides some properties and bindings API that make it relatively easy for your view to observe the model and stay in sync with it.

    A model for a Tic-tac-toe game should have an array or list of properties for each square, showing which player has "occupied" that square, along with other basic game state (which player plays next, has anyone won, is the game finished, etc.). There should be API for the current player to make a move in a given empty square. Since the state of the game is a function of the moves that have been made, it's probably best to expose the properties as read-only, and update them internally when moves are made.

    You can then implement the algorithm to check for a winner directly in the model. Using JavaFX bindings, you can make sure this is updated whenever the state of one of the squares changes.

    Here's a fairly basic implementation of this:

    First a simple enum for which player occupies a square:

    public enum Player { O, X, NONE }
    

    And then the main "model" class:

    import java.util.ArrayList;
    import java.util.List;
    
    import javafx.beans.Observable;
    import javafx.beans.binding.Bindings;
    import javafx.beans.binding.BooleanBinding;
    import javafx.beans.property.ReadOnlyBooleanProperty;
    import javafx.beans.property.ReadOnlyBooleanWrapper;
    import javafx.beans.property.ReadOnlyObjectProperty;
    import javafx.beans.property.ReadOnlyObjectWrapper;
    
    
    public class TicTacToe {
    
        private final List<ReadOnlyObjectWrapper<Player>> squares ;
        private final ReadOnlyObjectWrapper<Player> winner ;
        private final ReadOnlyObjectWrapper<Player> currentPlayer ;
        private final ReadOnlyBooleanWrapper gameOver ;
    
        public final static int SIZE = 3 ;
    
        public TicTacToe() {
            squares = new ArrayList<>();
            for (int i = 0 ; i < SIZE*SIZE ; i++) {
                squares.add(new ReadOnlyObjectWrapper<>(Player.NONE));
            }
    
            Observable[] squareArray = squares.toArray(new Observable[SIZE*SIZE]);
    
            winner = new ReadOnlyObjectWrapper<>(Player.NONE);
            winner.bind(Bindings.createObjectBinding(this::checkForWinner, squareArray));
    
            currentPlayer = new ReadOnlyObjectWrapper<>(Player.O);
    
            gameOver = new ReadOnlyBooleanWrapper() ;
            gameOver.bind(new BooleanBinding() {
    
                {
                    bind(winner);
                    bind(squareArray);
                }
    
                @Override
                protected boolean computeValue() {
                    return checkGameOver();
                }
    
            }); 
        }
    
        public ReadOnlyObjectProperty<Player> squareProperty(int square) {
            return squares.get(square).getReadOnlyProperty();
        }
    
        public final Player getSquare(int square) {
            return squareProperty(square).get();
        }
    
        public ReadOnlyObjectProperty<Player> winnerProperty() {
            return winner.getReadOnlyProperty();
        }
    
        public final Player getWinner() {
            return winnerProperty().get();
        }
    
        public ReadOnlyObjectProperty<Player> currentPlayerProperty() {
            return currentPlayer.getReadOnlyProperty();
        }
    
        public final Player getCurrentPlayer() {
            return currentPlayerProperty().get();
        }
    
        public ReadOnlyBooleanProperty gameOverProperty() {
            return gameOver.getReadOnlyProperty();
        }
    
        public final boolean isGameOver() {
            return gameOverProperty().get();
        }
    
        public boolean isEmpty(int square) {
            return getSquare(square) == Player.NONE ;
        }
    
        public void move(int square) {
            squares.get(square).set(currentPlayer.get());
            currentPlayer.set(opponent(currentPlayer.get()));
        }
    
        public void reset() {
            squares.forEach(s -> s.set(Player.NONE));
            currentPlayer.set(Player.O);
        }
    
        private boolean checkGameOver() {
            if (getWinner() != Player.NONE) return true ;
            return squares.stream()
                    .map(ReadOnlyObjectProperty::get)
                    .filter(Player.NONE::equals)
                    .findAny()
                    .isEmpty();
        }
    
        private Player checkForWinner() {
            // To do: if either player has won, return that player.
            // If no player has won (yet) return Player.NONE
            return Player.NONE ;
        }
    
    
        private Player opponent(Player player) {
            // Note Java 14 is required for switch expressions
            return switch(player) {
                case NONE -> Player.NONE ;
                case O -> Player.X ;
                case X -> Player.O ;
            };
        }
    
    }
    

    And now it's pretty easy to bind the UI to that model. E.g.:

    <?xml version="1.0" encoding="UTF-8"?>
    
    <?import javafx.scene.control.Button?>
    <?import javafx.scene.control.Label?>
    <?import javafx.scene.layout.BorderPane?>
    <?import javafx.scene.layout.GridPane?>
    <?import javafx.scene.layout.VBox?>
    <?import javafx.geometry.Insets?>
    
    <BorderPane xmlns="http://javafx.com/javafx/8.0.171"
        xmlns:fx="http://javafx.com/fxml/1"
        fx:controller="org.jamesd.examples.tictactoe.TicTacToeController">
    
        <top>
            <VBox styleClass="controls">
                <Label fx:id="winner" />
                <Button fx:id="reset" onAction="#reset" text="Reset" />
            </VBox>
        </top>
    
        <center>
            <GridPane fx:id="board" styleClass="board" alignment="CENTER">
            </GridPane>
        </center>
    </BorderPane>
    

    and the controller:

    import javafx.beans.property.ReadOnlyObjectProperty;
    import javafx.fxml.FXML;
    import javafx.scene.control.Button;
    import javafx.scene.control.Label;
    import javafx.scene.layout.GridPane;
    import javafx.scene.layout.Pane;
    import javafx.scene.layout.StackPane;
    
    public class TicTacToeController {
    
        private final TicTacToe game = new TicTacToe() ;
    
        @FXML
        private GridPane board ;
    
        @FXML
        private Label winner ;
        @FXML
        private Button reset ;
    
    
        public void initialize() {
            for (int row = 0 ; row < TicTacToe.SIZE ; row++) {
                for (int column = 0 ; column < TicTacToe.SIZE ; column++) {
                    board.add(createTile(row, column), column, row);
                }
            }
            winner.textProperty().bind(game.winnerProperty().asString("Winner: %s"));
            reset.disableProperty().bind(game.gameOverProperty().not());
        }
    
        @FXML
        private void reset() {
            game.reset();
        }
    
        public Pane createTile(int row, int column) {
            StackPane pane = new StackPane();
            pane.getStyleClass().add("square");
            Label o = new Label("O");
            Label x = new Label("X");
            pane.getChildren().addAll(o, x);
            int square = row * TicTacToe.SIZE + column;
            ReadOnlyObjectProperty<Player> player = game.squareProperty(square) ;
            o.visibleProperty().bind(player.isEqualTo(Player.O));
            x.visibleProperty().bind(player.isEqualTo(Player.X));
    
            pane.setOnMouseClicked(e -> {
                if (game.isEmpty(square) && ! game.isGameOver()) {
                    game.move(square);              
                }
            });
    
            return pane ;
        }
    
    }
    

    For completeness, the style classes hook into an external CSS file:

    .board {
      -fx-padding: 2 ;
    }
    
    .square {
      -fx-background-color: black, -fx-background ; 
      -fx-background-insets: 0, 1;
      -fx-min-width: 200 ;
      -fx-min-height: 200 ;
    }
    .square .label {
      -fx-font-size:108;
      -fx-font-family: comic-sans ;
      -fx-text-fill:#00b041 ;
    }
    .controls {
      -fx-spacing: 5 ;
      -fx-padding: 5 ;
      -fx-alignment: center ;
    }
    

    and the application startup class just looks like

    import java.io.IOException;
    
    import javafx.application.Application;
    import javafx.fxml.FXMLLoader;
    import javafx.scene.Scene;
    import javafx.stage.Stage;
    
    public class App extends Application {
    
    
        @Override
        public void start(Stage stage) throws IOException {
    
            FXMLLoader loader = new FXMLLoader(getClass().getResource("TicTacToeGrid.fxml"));
            Scene scene = new Scene(loader.load());
            scene.getStylesheets().add(getClass().getResource("tictactoe.css").toExternalForm());
            stage.setScene(scene);
            stage.show();
        }
    
    
        public static void main(String[] args) {
            launch();
        }
    
    }