Skip to content
Snippets Groups Projects
Handler.java 29.53 KiB
import javafx.animation.*;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Node;
import javafx.scene.control.Button;
import javafx.scene.control.ButtonType;
import javafx.scene.input.MouseButton;
import javafx.scene.layout.*;
import javafx.scene.paint.Color;
import javafx.scene.text.Font;
import javafx.scene.text.FontWeight;
import javafx.stage.Stage;
import javafx.util.Duration;
import java.util.ArrayList;

/**
 * this class handles the actions of the user when he interacts with the elements from the mathdoku stage
 * it manages the action of the buttons
 * it holds the method to draw the board of the game
 * it supervises the user interaction with the game, calculating results and compares the numbers introduced
 * of the player tot he rules of the game
 * when the game is solved correctly, the stage closes, and the application terminates
 */
public class Handler {

    private Window window = new Window(); //manager of the windows that must be opened in different circumstances
    private int n = Window.n; //size of board nxn
    static int[][] cellValues; //matrix of values of the cells
    private UndoRedo undoStack = new UndoRedo(); //stack to memorise the actions of the player
    private UndoRedo redoStack = new UndoRedo(); //stack to redo the actions the player had undone
    public static ArrayList<Cage> cages; //memorised the cages of the board

    /**
     * this is the constructor of the class which will set the cages array as a static variable
     * creates the matix of values of size nxn
     * @param cages -> are set to the static variable cages
     */
    public Handler(ArrayList<Cage> cages) {
        Handler.cages = cages;
        cellValues = new int[n][n];
    }

    /**
     * this method will draw the grid and the cages will be emphasized
     * @param grid -> the grid to sustain the cells
     */
    public void drawGrid(GridPane grid) {

        //loop through the cages
        for(Cage cage : cages) {
            //loop through the cells within a cage
            for(Cell cell : cage.getCells()) {

                //draw the cell depending on the neighbours it has in the cage
                if(cell.hasLeftNeighbour(cage) && cell.hasUpNeighbour(cage)) {
                    cell.setStyle("-fx-border-color: black; -fx-border-width: 0 4 4 0;");
                } else if(cell.hasLeftNeighbour(cage)) {
                    cell.setStyle("-fx-border-color: black; -fx-border-width: 4 4 4 0;");
                } else if(cell.hasUpNeighbour(cage)) {
                    cell.setStyle("-fx-border-color: black; -fx-border-width: 0 4 4 4;");
                } else {
                    cell.setStyle("-fx-border-color: black; -fx-border-width: 4 4 4 4;");
                }

                doubleClick(cell); //add the property to introduce values via mouse
                memoriseValues(cell); //memorise he values as the player introduces them in the cell
                grid.add(cell, cell.getColumn(), cell.getRow(), 1, 1); // add the cell to the grid
            }
        }
    }

    /**
     * method that will display a window which will permit the player to introduce or delete values via mouse
     * @param cell -> the cell to add the property to
     */
    public void doubleClick(Cell cell) {

        //mouse click event
        cell.getTextField().setOnMouseClicked(mouseEvent -> {
            if(mouseEvent.getButton().equals(MouseButton.PRIMARY)) {
                //count the number of mouse clicks, if they're 2 => show the button window
                if(mouseEvent.getClickCount() == 2) {
                    window.showButtons(cell); //show button window to introduce or delete a value from the cell
                }
            }
        });
    }

    /**
     * this method adds a listener to the text within a cell
     * interactively changes the values of the matrix depending on the values from the cells that are changed
     * @param cell -> cell to add the listener to
     */
    public void memoriseValues(Cell cell) {

        cell.getTextField().textProperty().addListener((observableValue, oldValue, newValue) -> {

            //check if the player entered a numeric value
            if (isNumeric(newValue)) {

                //check if the player introduced a number between 1 and n
                if (Integer.parseInt(newValue) > n || Integer.parseInt(newValue) == 0) {
                    cell.getTextField().setText(oldValue); //remain to the old value if not
                } else {
                    //the value is correct, set it to the cell and the matrix
                    cell.setValue(Integer.parseInt(newValue));
                    cellValues[cell.getRow()][cell.getColumn()] = Integer.parseInt(newValue);
                }
            } else if (!newValue.matches("\\d*")) {

                //the player introduced another character than a number => set the text to null and the matrix element to 0
                cell.getTextField().setText(newValue.replaceAll("[^\\d]", ""));
                cellValues[cell.getRow()][cell.getColumn()] = 0;
            }
        });
    }

    /**
     * method that manages the resizing of the board
     * @param root -> root of mathdoku stage
     * @param grid -> the board to resize
     */
    public void resize(VBox root, GridPane grid) {

        //add a listener to see when the width of the root is resized
        root.widthProperty().addListener((obs, oldVal, newVal) -> {

            grid.setMaxWidth((Double) newVal - 100); //set the new width of the grid

            //loop through the cages of the board
            for(Cage cage : cages) {
                //loop through the cells of a cage
                for(Cell cell : cage.getCells()) {
                    //resize each cell and the text field from inside it accordingly
                    cell.setPrefWidth((Double) newVal - 100);
                    cell.getTextField().setPrefSize(cell.getWidth(), cell.getHeight());
                }
            }
        });

        //add a listener to see when the height of the root is resized
        root.heightProperty().addListener((obs, oldVal, newVal) -> {

            grid.setMaxHeight((Double) newVal - 100); //set the new height of the grid

            //loop through the cages of the board
            for(Cage cage : cages) {
                //loop through the cells of the cage
                for(Cell cell : cage.getCells()) {
                    //resize each cell and the text field from inside it accordingly
                    cell.setPrefHeight((Double) newVal- 100);
                    cell.getTextField().setPrefSize(cell.getWidth(), cell.getHeight());
                }
            }
        });
    }

    /**
     * method that manages the help action from the mathdoku stage
     * @param help -> the button to add the action to
     * @param grid -> the grid changes properties depending on the help option activated (enabled or not)
     */
    public void helpButton(Button help, GridPane grid) {

        help.setOnAction(actionEvent -> {

            //the button will change the text depending on the help option is enabled or not
            if(help.getText().equals("Enable Help")) {

                help.setText("Disable Help"); //the help button is enabled => set the text to "Disable Help"
                highlight(grid);//highlight the grid accordingly (incorrect cages with red, incorrect rows or
                                // columns with yellow

                //interactively highlight the board depending on the values introduced to the cells
                for(Node cell : grid.getChildren()) {
                    enableHighlight((Cell) cell, grid);
                }
            } else {

                help.setText("Enable Help"); //the help button is disabled => set the text to "Enable Help"
                setTransparent(grid); //make the whole grid transparent

                //disable the highlight option for each cell
                for(Node cell : grid.getChildren()) {
                    disableHighlight((Cell) cell, grid);
                }
            }
        });
    }

    /**
     * this method will enable the highlight of the board accordingly
     * @param cell -> the cell to add the listener, it will be highlighted interactively
     * @param grid -> the board to highlight
     */
    public void enableHighlight(Cell cell, GridPane grid) {

        cell.getTextField().textProperty().addListener((observableValue, s, t1) -> {

            setTransparent(grid); //set all the board transparent before highlighting
            highlight(grid); //highlight the grid
        });
    }

    /**
     * this method disables the highlight option
     * @param cell -> the cell to add the listener
     * @param grid -> the board to set to be transparent
     */
    public void disableHighlight(Cell cell, GridPane grid) {
        cell.getTextField().textProperty().addListener((observableValue, s, t1) -> setTransparent(grid));
    }

    /**
     * this method highlights the grid accordingly
     * @param grid to be highlighted
     */
    public void highlight(GridPane grid) {

        //loop through the the cells in the grid
        for(int i = 0; i < n; i ++) {
            for(int j = 0; j < n; j ++) {

                //check for cells that contain a value
                if(cellValues[i][j] != 0) {

                    //highlight the row if the row doesn't consist of unique numbers
                    if(!verifyRow(cellValues[i][j], i)) {
                        highlightRow(i, grid);
                    }

                    //highlight the column if the column doesn't consist of unique numbers
                    if(!verifyColumn(cellValues[i][j], j)) {
                        highlightColumn(j, grid);
                    }
                }
            }
        }

        //highlight the cages
        for(Cage cage : cages) {
            highlightCage(cage);
        }
    }

    /**
     * this method highlights a specific row from the board if it doesn't contain unique numbers
     * @param row to be highlighted
     * @param grid which contains the row to be highlighted
     */
    public void highlightRow(int row, GridPane grid) {

        //loop through the cells of that row and highlight them with yellow
        for(int i = 0; i < n; i ++) {
            getCell(grid, row, i).setBackground(new Background(new BackgroundFill(Color.YELLOW,
                    CornerRadii.EMPTY, Insets.EMPTY)));
        }
    }

    /**
     * this method highlights a specific column from the board if it doesn't contain unique numbers
     * @param column to be highlighted
     * @param grid which contains the column to be highlighted
     */
    public void highlightColumn(int column, GridPane grid) {

        //loop through the cells of the column and highlight them with yellow
        for(int i = 0; i < n; i ++) {
            getCell(grid, i, column).setBackground(new Background(new BackgroundFill(Color.YELLOW,
                    CornerRadii.EMPTY, Insets.EMPTY)));
        }
    }

    /**
     * this method highlights a cage if it the values within the cells of the cage don't satisfy the result
     * @param cage to be highlighted
     */
    public void highlightCage(Cage cage) {
        //verify if the values satisfy the result of the cage
        if(!verifyCage(cage)) {
            //loop through the cells of the cage to highlight them with red
            for(Cell cell : cage.getCells()) {
                cell.setBackground(new Background(new BackgroundFill(Color.RED,
                        CornerRadii.EMPTY, Insets.EMPTY)));
            }
        }
    }

    /**
     * this method makes the board transparent
     * helps with managing the help button
     * @param grid to be set transparent
     */
    public void setTransparent(GridPane grid) {

        //loop through the cells from the grid
        for(Node cell : grid.getChildren()) {
            ((Cell) cell).setBackground(new Background(new BackgroundFill(Color.TRANSPARENT,
                    CornerRadii.EMPTY, Insets.EMPTY))); //make each cell transparent
        }
    }

    /**
     * method that manages the submit button from the mathdoku stage
     * @param submit -> the button to add the action to
     * @param stage -> necessary for closing the mathdoku stage if the game is correctly completed
     */
    public void submitButton(Button submit, Stage stage) {
        submit.setOnAction(actionEvent -> {

            boolean isCompleted = true; //will verify if the board is completed

            //loop through the cages and break if there exist a cage that is not completed
            for(Cage cage : cages) {
                if(!cage.isCompleted()) {
                    window.win("Complete de board!", "Don't stop now...").show(); //show window with appropriate message
                    isCompleted = false; //is not completed => set it to false
                    break;
                }
            }

            if(isCompleted) {
                //check if the board is correctly completed
                if(verify() && verifyCages()) {
                    win(); //animation => each cell randomly changes colors
                    window.win("Congratulations!", "You are the master!").showAndWait(); //show appropriate message
                    stage.close(); //close the mathdoku stage when the window with the message is closed
                } else {
                    //the board is not correctly completed
                    window.win("Try again...", "...it's not correct.").show(); //show message in the win window
                }
            }
        });
    }

    /**
     * this is the animation that will be displayed when the player wins the game
     * each cell from the board randomly will be colored differently with a time of changing of 100 milliseconds
     * it will look like a disco ball :)
     */
    public void win() {

        //create the timeline for managing the animation
        Timeline timeline = new Timeline(new KeyFrame(Duration.millis(100), actionEvent -> {

            //loop through the cages and the cells to change the color randomly
            for(Cage cage : cages) {
                for(Cell cell : cage.getCells()) {
                    cell.setBackground(new Background(new BackgroundFill(Color.color(Math.random(), Math.random(),
                            Math.random()), CornerRadii.EMPTY, Insets.EMPTY)));
                }
            }
        }));

        timeline.setCycleCount(Transition.INDEFINITE); //make the changing of color go infinitely
        timeline.play(); //start the time line
    }

    /**
     * method that manages the clear button from the mathdoku stage
     * @param clear -> the button to add the action to
     * @param grid -> necessary to clear the board of the game
     * @param undo -> the undo button must be disabled
     * @param redo -> the redo button must be disabled
     */
    public void clearBoard(Button clear, GridPane grid, Button undo, Button redo) {

        //display a confirmation of action window so the player will confirm to clear the board
        clear.setOnAction(actionEvent -> window.alertDialog("Are you sure you want to clear " +
                "the board?").showAndWait().ifPresent(response -> {
            //if the response is "YES" => clear board
            if(response == ButtonType.YES) {
                clear(grid);
                undo.setDisable(true);
                redo.setDisable(true);
            }
        }));
    }

    /**
     * this method clears the board -> sets the values from the cells to null
     * @param grid -> the grid to clear
     */
    public void clear(GridPane grid) {

        //loop through the cells of the grid
        for(int i =0; i < n; i ++) {
            for(int j = 0; j < n; j ++) {

                cellValues[i][j] = 0; //set the value in the matrix at that position to 0
                getCell(grid, i, j).getTextField().clear(); //clear the text of the cell
                getCell(grid, i, j).setBackground(new Background(new BackgroundFill(Color.TRANSPARENT,
                        CornerRadii.EMPTY, Insets.EMPTY))); //make the cell transparent again
            }
        }

        //the undo and redo stacks point now to null
        undoStack.root = null;
        redoStack.root = null;
    }

    /**
     * this method manages the undo button from the mathdoku stage
     * @param undo -> button to add the action to
     * @param redo -> needed to enable the redo button
     * @param grid -> to get a specific cell from the grid
     */
    public void undoAction(Button undo, Button redo, GridPane grid) {

        undo.setOnAction(actionEvent -> {

            //disable the undo button when the undo stack is empty => can't do any other undo action
            if(undoStack.isEmpty()) {
                undo.setDisable(true);
            } else {
                redo.setDisable(false); //enable the redo action
                Element e = undoStack.pop(); //pop an element from the undo stack
                redoStack.push(e);//push that element to the redo stack
                getCell(grid, e.i, e.j).getTextField().clear(); //clear the content from that specific cell
                cellValues[e.i][e.j] = 0; //set the value of the matrix at that position
            }
        });
    }

    /**
     * this method manages the redo button from the mathdoku stage
     * @param redo -> button to add the action to
     * @param grid -> to get the specific cell from the grid
     */
    public void redoAction(Button redo, GridPane grid) {

        redo.setOnAction(actionEvent -> {

            //disable the redo button when the redo stack is empty => can't do any redo action
            if(redoStack.isEmpty()) {
                redo.setDisable(true);
            } else {
                Element e = redoStack.pop(); //pop the element from the redo stack
                getCell(grid, e.i, e.j).getTextField().setText(String.valueOf(e.value)); //set the content of that
                                                                    // cell to the element popped from the redo stack
                undoStack.push(e); //push the element back to the undo stack
            }
        });
    }

    /**
     * method that will be responsible with adding the player's actions to the undo stack
     * @param grid -> to loop through the cells from the grid
     * @param undo -> to enable the undo stack
     */
    public void addActions(GridPane grid, Button undo) {

        //loop through the cells of the board
        for(Node cell : grid.getChildren()) {

            //add a listener to the text of the cells to add the numbers introduced to the undo stack
            ((Cell) cell).getTextField().textProperty().addListener((observableValue, s, t1) -> {

                //only add the numeric values
                if(isNumeric(((Cell) cell).getTextField().getText())) {
                    Element e = new Element(((Cell) cell).getRow(), ((Cell) cell).getColumn(),
                            Integer.parseInt(((Cell) cell).getTextField().getText())); //create the element that must
                                                                                // be added to the undo stack
                    undoStack.push(e); //push that element to the undo stack
                    undo.setDisable(false); //enable the undo button
                }
            });
        }
    }

    /**
     * method that manages the font size change
     * @param button -> press to change size of the text within a cell and the label of a cage
     * @param number -> the number to use to change the size of the text
     */
    public void changeFont(Button button, int number) {

        button.setOnAction(actionEvent -> {

            //loop through the cages of the board
            for(Cage cage : cages) {
                //loop through the cells of the board
                for(Cell cell : cage.getCells()) {

                    //set the new properties of the text
                    cell.getTextField().setStyle("-fx-text-box-border: transparent; " +
                            "-fx-background-color: transparent;");
                    cell.getTextField().setFont(Font.font("Verdana", FontWeight.BOLD, number));
                }

                //change the size of the label of the cage
                cage.getCells().get(0).getLabel().setFont(Font.font(number - 10));
            }
        });
    }

    /**
     * this method helps with the functionality of the submit button
     * determines if the board satisfied the property of having unique individual numbers on the row and column
     * @return boolean depending if the row and column both satisfy the property
     */
    public boolean verify() {

        //loop through the values of the matrix of the values of the cells
        for(int i = 0; i < n; i ++) {
            for(int j = 0; j < n; j ++) {
                //verify for cells that are completed
                if(cellValues[i][j] !=0 ) {
                    //if the number is not unique on the row or the column, return false
                    if(!verifyRow(cellValues[i][j], i) || !verifyColumn(cellValues[i][j], j)) {
                        return false;
                    }
                }
            }
        }

        return true; //return true when the property is satisfied
    }

    /**
     * this method verifies if the row contains only one unique value of a number
     * @param x -> the number to verify if it appears only once in the row
     * @param row -> the row to verify
     * @return true if the number x only appears once in the row
     */
    public boolean verifyRow(int x, int row) {

        int count = 0; //set the number of appearances to 0

        //loop in the matrix of values on the row specified
        for(int i = 0; i < n; i ++) {
            if(cellValues[row][i] == x) {
                count++; //count whenever a number in the row is equal to the number to be verified
            }
        }

        return count == 1; //if it only appears once, return true, else return false
    }

    /**
     * this method verifies if the column contains only one unique value of a number
     * @param x -> the number to verify if it appears only once in the column
     * @param column -> the column to verify
     * @return true if the number x only appears once in the column
     */
    public boolean verifyColumn(int x, int column) {

        int count = 0; //set the number of appearances to 0

        //loop in the matrix of values on the column specified
        for(int i = 0; i < n; i ++) {
            if(cellValues[i][column] == x) {
                count++; //count whenever a number in the column is equal to the number to be verified
            }
        }

        return count == 1; //if it only appears once, return true, else return false
    }

    /**
     * this method helps with the functionality of the submit button
     * it verifies if all the cages contain cells with values that satisfy the result
     * @return true if all the cages satisfy the condition, else false
     */
    static boolean verifyCages() {

        for(Cage cage : cages) {
            if(!verifyCage(cage)) {
                return false;
            }
        }
        return true;
    }

    /**
     * this method verifies is the values from the cells within a cage satisfy the operator of the cage and the result
     * @param cage -> the cage to be verified
     * @return true if the result is correct
     */
    public static boolean verifyCage(Cage cage) {

        boolean isCorrect = true;

        //if the cage is not complete, the method will return true
        if(cage.isCompleted()) {

            //if the cage doesn't have a symbol => the result from the cell must be the same as the symbol,
            // else return false
            if(!cage.hasSymbol()) {
                if(Integer.parseInt(cage.getCells().get(0).getTextField().getText()) != Integer.parseInt(cage.getResult()) &&
                        Integer.parseInt(cage.getCells().get(0).getTextField().getText()) != 0) {
                    isCorrect = false;
                }
            } else if(cage.getSymbol() == '+' && !verifySum(cage)) {
                isCorrect = false;
            } else if((cage.getSymbol() == 'x' || cage.getSymbol() == '*') && !verifyProduct(cage)) {
                isCorrect = false;
            } else if(cage.getSymbol() == '-' && !verifySubtraction(cage)) {
                isCorrect = false;
            } else if((cage.getSymbol() == '/' || cage.getSymbol() == '÷') && !verifyDivision(cage)) {
                isCorrect = false;
            }
        }

        return isCorrect;
    }

    /**
     * this method verifies a cage when the symbol for that cage is '+'
     * @param cage -> cage to be verified
     * @return true if the sum of the cells in the cage have the correct result of the cage
     */
    public static boolean verifySum(Cage cage) {

        int sum = 0; //initialise the sum with 0

        //loop through the cells of the cage
        for(Cell cell : cage.getCells()) {
            sum = sum + Integer.parseInt(cell.getTextField().getText()); //sum the value sof the cells from the cage
        }

        return (sum == Integer.parseInt(cage.getResult())); //return true if the results are the same
    }

    /**
     * this method verifies a cage when the symbol for the cage is 'x' or '*'
     * @param cage -> cage to be verified
     * @return true if the product of the values of the cells from the cage has the correct result
     */
    public static boolean verifyProduct(Cage cage) {

        int product = 1; //initialise the product with 1

        //loop through the cells of the cage
        for(Cell cell : cage.getCells()) {
            product = product * Integer.parseInt(cell.getTextField().getText()); //calculate the product of the values
        }

        return ((product == Integer.parseInt(cage.getResult()))); //return true if the results are the same
    }

    /**
     * this method verifies a cage when the symbol for that cage is '-'
     * sort the values of the cells from the cage in descending order and subtract in order
     * @param cage -> cage to be verified
     * @return true if the subtraction of the values of the cells from the cage has the correct result
     */
    public static boolean verifySubtraction(Cage cage) {

        int x = cage.getValues()[0]; //get the first value of the descending sorted array of the
                                    // values of the cells from the cage

        //loop through the values of the descending sorted array of the values of the cells from the cage
        for(int i = 1; i < cage.getValues().length; i ++) {
            x = x - cage.getValues()[i]; //subtract each value from the result
        }

        return (x == Integer.parseInt(cage.getResult())); //return true if the results correspond
    }

    /**
     * this method verifies a cage when the symbol for the cage is '/' or '÷'
     * sort the values of the cells of the cage in descending order and divide in order
     * @param cage -> cage to be verified
     * @return true if the division of the values of the cells from the cage has the correct result
     */
    public static boolean verifyDivision(Cage cage) {

        int x = cage.getValues()[0]; //get the first value of the descending sorted array of the
                                        // values of the cells form the cage

        //loop through the values of the descending sorted array of the values of the cells from the cage
        for(int i = 1; i < cage.getValues().length; i ++) {
            //if the module operator doesn't result in 0, then return false
            if(x % cage.getValues()[i] == 0) {
                x = x / cage.getValues()[i]; //else, divide the greatest number (x) to the next value in the array
            } else {
                return false;
            }
        }

        return (x == Integer.parseInt(cage.getResult())); //return true if the results correspond
    }

    /**
     * this method will return the cell from a grid pane, placed at a specific position (i, j)
     * @param grid -> the grid which contains the cell
     * @param i -> number of the row position
     * @param j -> number of the column position
     * @return the cell at the corresponding position in the grid
     */
    static Cell getCell(GridPane grid, int i, int j) {

        //loop through the cells of the grid
        for(Node cell : grid.getChildren()) {

            //return the cell at the position (i,j)
            if(((Cell) cell).getRow() == i && ((Cell) cell).getColumn() == j) {
                return (Cell) cell;
            }
        }

        return null; //return null when the cell doesn't exist in the grid
    }

    /**
     * this method verifies if the number within a cage is numeric
     * @param x -> the value to be verified
     * @return true if there exists no exception when trying to convert the value into an integer, false otherwise
     */
    public static boolean isNumeric(String x) {
        try {
            Integer.parseInt(x);
        } catch (NumberFormatException e) {
            return false;
        }
        return true;
    }

    public void solveBoard(GridPane grid) {

        Solver solver = new Solver(n, grid);
        solver.backtracking(1, 1);
        clear(grid);
    }

    public void hint(Button hint, GridPane grid) {

        hint.setOnAction(actionEvent -> {

            boolean found = false;

            for(int i = 0; i < n && !found; i ++) {
                for(int j = 0; j < n; j ++) {
                    if(cellValues[i][j] != Solver.solved[i][j]) {
                        getCell(grid, i, j).setValue(Solver.solved[i][j]);
                        cellValues[i][j] = Solver.solved[i][j];
                        found = true;
                        break;
                    }
                }
            }
        });
    }
}