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