package io.fxgame.game2048; import java.time.LocalTime; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.Arrays; import java.util.Map; import java.util.Set; import javafx.animation.Animation; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.Timeline; import javafx.application.Platform; import javafx.beans.binding.Bindings; import javafx.beans.property.BooleanProperty; import javafx.beans.property.IntegerProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.beans.property.SimpleIntegerProperty; import javafx.beans.property.SimpleStringProperty; import javafx.beans.property.StringProperty; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; import javafx.beans.value.WritableBooleanValue; import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Group; import javafx.scene.control.Button; import javafx.scene.control.Hyperlink; import javafx.scene.control.Label; import javafx.scene.layout.HBox; import javafx.scene.layout.Pane; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; import javafx.scene.paint.Color; import javafx.scene.shape.Rectangle; import javafx.scene.text.Text; import javafx.scene.text.TextAlignment; import javafx.scene.text.TextFlow; import javafx.util.Duration; /** * @author Jose Pereda * @author Bruno Borges */ public class Board extends Pane { public static final int CELL_SIZE = 128; private static final int BORDER_WIDTH = (14 + 2) / 2; private static final int TOP_HEIGHT = 92; private static final int GAP_HEIGHT = 50; private static final int TOOLBAR_HEIGHT = 80; private final IntegerProperty gameScoreProperty = new SimpleIntegerProperty(0); private final IntegerProperty gameBestProperty = new SimpleIntegerProperty(0); private final IntegerProperty gameMovePoints = new SimpleIntegerProperty(0); private final BooleanProperty gameWonProperty = new SimpleBooleanProperty(false); private final BooleanProperty gameOverProperty = new SimpleBooleanProperty(false); private final BooleanProperty gameAboutProperty = new SimpleBooleanProperty(false); private final BooleanProperty gamePauseProperty = new SimpleBooleanProperty(false); private final BooleanProperty gameTryAgainProperty = new SimpleBooleanProperty(false); private final BooleanProperty gameSaveProperty = new SimpleBooleanProperty(false); private final BooleanProperty gameRestoreProperty = new SimpleBooleanProperty(false); private final BooleanProperty gameQuitProperty = new SimpleBooleanProperty(false); private final BooleanProperty layerOnProperty = new SimpleBooleanProperty(false); private final BooleanProperty resetGame = new SimpleBooleanProperty(false); private final BooleanProperty clearGame = new SimpleBooleanProperty(false); private final BooleanProperty restoreGame = new SimpleBooleanProperty(false); private final BooleanProperty saveGame = new SimpleBooleanProperty(false); private LocalTime time; private Timeline timer; private final StringProperty clock = new SimpleStringProperty("00:00:00"); private final DateTimeFormatter fmt = DateTimeFormatter.ofPattern("HH:mm:ss").withZone(ZoneId.systemDefault()); // User Interface controls private final VBox vGame = new VBox(0); private final Group gridGroup = new Group(); private final HBox hTop = new HBox(0); private final VBox vScore = new VBox(-5); private final Label lblScore = new Label("0"); private final Label lblBest = new Label("0"); private final Label lblPoints = new Label(); private final HBox overlay = new HBox(); private final VBox txtOverlay = new VBox(10); private final Label lOvrText = new Label(); private final Label lOvrSubText = new Label(); private final HBox buttonsOverlay = new HBox(); // Overlay Buttons private final Button bTry = new Button("Try again"); private final Button bContinue = new Button("Keep going"); private final Button bContinueNo = new Button("No, keep going"); private final Button bSave = new Button("Save"); private final Button bRestore = new Button("Restore"); private final Button bQuit = new Button("Quit"); private final HBox hToolbar = new HBox(); private final Label lblTime = new Label(); private final int gridWidth; private final GridOperator gridOperator; private final SessionManager sessionManager; public Board(GridOperator grid) { this.gridOperator = grid; gridWidth = CELL_SIZE * grid.getGridSize() + BORDER_WIDTH * 2; sessionManager = new SessionManager(gridOperator); createScore(); createGrid(); createToolBar(); initGameProperties(); } private void createScore() { var lblTitle = new Label("2048"); lblTitle.getStyleClass().addAll("game-label", "game-title"); var lblSubtitle = new Label("FX"); lblSubtitle.getStyleClass().addAll("game-label", "game-subtitle"); var hFill = new HBox(); HBox.setHgrow(hFill, Priority.ALWAYS); hFill.setAlignment(Pos.CENTER); var vScores = new VBox(); var hScores = new HBox(5); vScore.setAlignment(Pos.CENTER); vScore.getStyleClass().add("game-vbox"); var lblTit = new Label("SCORE"); lblTit.getStyleClass().addAll("game-label", "game-titScore"); lblScore.getStyleClass().addAll("game-label", "game-score"); lblScore.textProperty().bind(gameScoreProperty.asString()); vScore.getChildren().addAll(lblTit, lblScore); var vRecord = new VBox(-5); vRecord.setAlignment(Pos.CENTER); vRecord.getStyleClass().add("game-vbox"); var lblTitBest = new Label("BEST"); lblTitBest.getStyleClass().addAll("game-label", "game-titScore"); lblBest.getStyleClass().addAll("game-label", "game-score"); lblBest.textProperty().bind(gameBestProperty.asString()); vRecord.getChildren().addAll(lblTitBest, lblBest); hScores.getChildren().addAll(vScore, vRecord); var vFill = new VBox(); VBox.setVgrow(vFill, Priority.ALWAYS); vScores.getChildren().addAll(hScores, vFill); hTop.getChildren().addAll(lblTitle, lblSubtitle, hFill, vScores); hTop.setMinSize(gridWidth, TOP_HEIGHT); hTop.setPrefSize(gridWidth, TOP_HEIGHT); hTop.setMaxSize(gridWidth, TOP_HEIGHT); vGame.getChildren().add(hTop); var hTime = new HBox(); hTime.setMinSize(gridWidth, GAP_HEIGHT); hTime.setAlignment(Pos.BOTTOM_RIGHT); lblTime.getStyleClass().addAll("game-label", "game-time"); lblTime.textProperty().bind(clock); timer = new Timeline(new KeyFrame(Duration.ZERO, e -> { clock.set(LocalTime.now().minusNanos(time.toNanoOfDay()).format(fmt)); }), new KeyFrame(Duration.seconds(1))); timer.setCycleCount(Animation.INDEFINITE); hTime.getChildren().add(lblTime); vGame.getChildren().add(hTime); getChildren().add(vGame); lblPoints.getStyleClass().addAll("game-label", "game-points"); lblPoints.setAlignment(Pos.CENTER); lblPoints.setMinWidth(100); getChildren().add(lblPoints); } private Rectangle createCell(int i, int j) { final double arcSize = CELL_SIZE / 6d; var cell = new Rectangle(i * CELL_SIZE, j * CELL_SIZE, CELL_SIZE, CELL_SIZE); // provide default style in case css are not loaded cell.setFill(Color.WHITE); cell.setStroke(Color.GREY); cell.setArcHeight(arcSize); cell.setArcWidth(arcSize); cell.getStyleClass().add("game-grid-cell"); return cell; } private void createGrid() { gridOperator.traverseGrid((i, j) -> { gridGroup.getChildren().add(createCell(i, j)); return 0; }); gridGroup.getStyleClass().add("game-grid"); gridGroup.setManaged(false); gridGroup.setLayoutX(BORDER_WIDTH); gridGroup.setLayoutY(BORDER_WIDTH); var hBottom = new HBox(); hBottom.getStyleClass().add("game-backGrid"); hBottom.setMinSize(gridWidth, gridWidth); hBottom.setPrefSize(gridWidth, gridWidth); hBottom.setMaxSize(gridWidth, gridWidth); // Clip hBottom to keep the dropshadow effects within the hBottom var rect = new Rectangle(gridWidth, gridWidth); hBottom.setClip(rect); hBottom.getChildren().add(gridGroup); vGame.getChildren().add(hBottom); } private void createToolBar() { // toolbar var hPadding = new HBox(); hPadding.setMinSize(gridWidth, TOOLBAR_HEIGHT); hPadding.setPrefSize(gridWidth, TOOLBAR_HEIGHT); hPadding.setMaxSize(gridWidth, TOOLBAR_HEIGHT); hToolbar.setAlignment(Pos.CENTER); hToolbar.getStyleClass().add("game-backGrid"); hToolbar.setMinSize(gridWidth, TOOLBAR_HEIGHT); hToolbar.setPrefSize(gridWidth, TOOLBAR_HEIGHT); hToolbar.setMaxSize(gridWidth, TOOLBAR_HEIGHT); vGame.getChildren().add(hPadding); vGame.getChildren().add(hToolbar); } protected void setToolBar(HBox toolbar) { toolbar.disableProperty().bind(layerOnProperty); toolbar.spacingProperty().bind(Bindings.divide(vGame.widthProperty(), 10)); hToolbar.getChildren().add(toolbar); } protected void tryAgain() { if (!gameTryAgainProperty.get()) { gameTryAgainProperty.set(true); } } private void btnTryAgain() { layerOnProperty.set(false); doResetGame(); } private void keepGoing() { layerOnProperty.set(false); gamePauseProperty.set(false); gameTryAgainProperty.set(false); gameSaveProperty.set(false); gameRestoreProperty.set(false); gameAboutProperty.set(false); gameQuitProperty.set(false); timer.play(); } private void exitGame() { Platform.exit(); } private final Overlay wonListener = new Overlay("You win!", "", bContinue, bTry, "game-overlay-won", "game-lblWon"); private class Overlay implements ChangeListener<Boolean> { private final Button btn1, btn2; private final String message, warning; private final String style1, style2; public Overlay(String message, String warning, Button btn1, Button btn2, String style1, String style2) { this.message = message; this.warning = warning; this.btn1 = btn1; // left this.btn2 = btn2; // right this.style1 = style1; this.style2 = style2; } @Override public void changed(ObservableValue<? extends Boolean> observable, Boolean oldValue, Boolean newValue) { if (!newValue) { return; } timer.stop(); overlay.getStyleClass().setAll("game-overlay", style1); lOvrText.setText(message); lOvrText.getStyleClass().setAll("game-label", style2); lOvrSubText.setText(warning); lOvrSubText.getStyleClass().setAll("game-label", "game-lblWarning"); txtOverlay.getChildren().setAll(lOvrText, lOvrSubText); buttonsOverlay.getChildren().setAll(btn1); if (btn2 != null) { buttonsOverlay.getChildren().add(btn2); } if (!layerOnProperty.get()) { var defaultBtn = btn2 == null ? btn1 : btn2; defaultBtn.requestFocus(); defaultBtn.setDefaultButton(true); Board.this.getChildren().addAll(overlay, buttonsOverlay); layerOnProperty.set(true); } } } private void initGameProperties() { overlay.setMinSize(gridWidth, gridWidth); overlay.setAlignment(Pos.CENTER); overlay.setTranslateY(TOP_HEIGHT + GAP_HEIGHT); overlay.getChildren().setAll(txtOverlay); txtOverlay.setAlignment(Pos.CENTER); buttonsOverlay.setAlignment(Pos.CENTER); buttonsOverlay.setTranslateY(TOP_HEIGHT + GAP_HEIGHT + gridWidth / 2); buttonsOverlay.setMinSize(gridWidth, gridWidth / 2); buttonsOverlay.setSpacing(10); bTry.getStyleClass().add("game-button"); bTry.setOnAction(e -> btnTryAgain()); bContinue.getStyleClass().add("game-button"); bContinue.setOnAction(e -> keepGoing()); bContinueNo.getStyleClass().add("game-button"); bContinueNo.setOnAction(e -> keepGoing()); bSave.getStyleClass().add("game-button"); bSave.setOnAction(e -> saveGame.set(true)); bRestore.getStyleClass().add("game-button"); bRestore.setOnAction(e -> restoreGame.set(true)); bQuit.getStyleClass().add("game-button"); bQuit.setOnAction(e -> exitGame()); gameWonProperty.addListener(wonListener); gameOverProperty .addListener(new Overlay("Game over!", "", bTry, null, "game-overlay-over", "game-lblOver")); gamePauseProperty.addListener( new Overlay("Game Paused", "", bContinue, null, "game-overlay-pause", "game-lblPause")); gameTryAgainProperty.addListener(new Overlay("Try Again?", "Current game will be deleted", bTry, bContinueNo, "game-overlay-pause", "game-lblPause")); gameSaveProperty.addListener(new Overlay("Save?", "Previous saved data will be overwritten", bSave, bContinueNo, "game-overlay-pause", "game-lblPause")); gameRestoreProperty.addListener(new Overlay("Restore?", "Current game will be deleted", bRestore, bContinueNo, "game-overlay-pause", "game-lblPause")); gameAboutProperty.addListener((observable, oldValue, newValue) -> { if (newValue) { timer.stop(); overlay.getStyleClass().setAll("game-overlay", "game-overlay-quit"); TextFlow flow = new TextFlow(); flow.setTextAlignment(TextAlignment.CENTER); flow.setPadding(new Insets(10, 0, 0, 0)); flow.setMinSize(gridWidth, gridWidth); flow.setPrefSize(gridWidth, gridWidth); flow.setMaxSize(gridWidth, gridWidth); flow.setPrefSize(BASELINE_OFFSET_SAME_AS_HEIGHT, BASELINE_OFFSET_SAME_AS_HEIGHT); var t00 = new Text("2048"); t00.getStyleClass().setAll("game-label", "game-lblAbout"); var t01 = new Text("FX"); t01.getStyleClass().setAll("game-label", "game-lblAbout2"); var t02 = new Text(" Game\n"); t02.getStyleClass().setAll("game-label", "game-lblAbout"); var t1 = new Text("JavaFX game - Desktop version\n\n"); t1.getStyleClass().setAll("game-label", "game-lblAboutSub"); var t20 = new Text("Powered by "); t20.getStyleClass().setAll("game-label", "game-lblAboutSub"); var link1 = new Hyperlink(); link1.setText("OpenJFX"); link1.setOnAction(e -> Game2048.urlOpener().open("https://openjfx.io/")); link1.getStyleClass().setAll("game-label", "game-lblAboutSub2"); var t21 = new Text(" Project \n\n"); t21.getStyleClass().setAll("game-label", "game-lblAboutSub"); var t23 = new Text("\u00A9 "); t23.getStyleClass().setAll("game-label", "game-lblAboutSub"); var link2 = new Hyperlink(); link2.setText("@JPeredaDnr"); link2.setOnAction(e -> Game2048.urlOpener().open("https://twitter.com/JPeredaDnr")); link2.getStyleClass().setAll("game-label", "game-lblAboutSub2"); var t22 = new Text(" & "); t22.getStyleClass().setAll("game-label", "game-lblAboutSub"); var link3 = new Hyperlink(); link3.setText("@brunoborges"); link3.setOnAction(e -> Game2048.urlOpener().open("https://twitter.com/brunoborges")); var t32 = new Text(" & "); t32.getStyleClass().setAll("game-label", "game-lblAboutSub"); link3.getStyleClass().setAll("game-label", "game-lblAboutSub2"); var t24 = new Text("\n\n"); t24.getStyleClass().setAll("game-label", "game-lblAboutSub"); var t31 = new Text(" Version " + Game2048.VERSION + " - 2015\n\n"); t31.getStyleClass().setAll("game-label", "game-lblAboutSub"); flow.getChildren().setAll(t00, t01, t02, t1, t20, link1, t21, t23, link2, t22, link3); flow.getChildren().addAll(t24, t31); txtOverlay.getChildren().setAll(flow); buttonsOverlay.getChildren().setAll(bContinue); this.getChildren().removeAll(overlay, buttonsOverlay); this.getChildren().addAll(overlay, buttonsOverlay); layerOnProperty.set(true); } }); gameQuitProperty.addListener(new Overlay("Quit Game?", "Non saved data will be lost", bQuit, bContinueNo, "game-overlay-quit", "game-lblQuit")); restoreRecord(); gameScoreProperty.addListener((ov, i, i1) -> { if (i1.intValue() > gameBestProperty.get()) { gameBestProperty.set(i1.intValue()); } }); layerOnProperty.addListener((ov, b, b1) -> { if (!b1) { getChildren().removeAll(overlay, buttonsOverlay); // Keep the focus on the game when the layer is removed: getParent().requestFocus(); } else if (b1) { // Set focus on the first button buttonsOverlay.getChildren().get(0).requestFocus(); } }); } private void doClearGame() { saveRecord(); gridGroup.getChildren().removeIf(c -> c instanceof Tile); getChildren().removeAll(overlay, buttonsOverlay); Arrays.asList(clearGame, resetGame, restoreGame, saveGame, layerOnProperty, gameWonProperty, gameOverProperty, gameAboutProperty, gamePauseProperty, gameTryAgainProperty, gameSaveProperty, gameRestoreProperty, gameQuitProperty).forEach(a -> ((WritableBooleanValue) a).set(false)); gameScoreProperty.set(0); clearGame.set(true); } private void doResetGame() { doClearGame(); resetGame.set(true); } public void animateScore() { if (gameMovePoints.get() == 0) { return; } final var timeline = new Timeline(); lblPoints.setText("+" + gameMovePoints.getValue().toString()); lblPoints.setOpacity(1); double posX = vScore.localToScene(vScore.getWidth() / 2d, 0).getX(); lblPoints.setTranslateX(0); lblPoints.setTranslateX(lblPoints.sceneToLocal(posX, 0).getX() - lblPoints.getWidth() / 2d); lblPoints.setLayoutY(20); final var kvO = new KeyValue(lblPoints.opacityProperty(), 0); final var kvY = new KeyValue(lblPoints.layoutYProperty(), 100); var animationDuration = Duration.millis(600); final KeyFrame kfO = new KeyFrame(animationDuration, kvO); final KeyFrame kfY = new KeyFrame(animationDuration, kvY); timeline.getKeyFrames().add(kfO); timeline.getKeyFrames().add(kfY); timeline.play(); } public void addTile(Tile tile) { double layoutX = tile.getLocation().getLayoutX(CELL_SIZE) - (tile.getMinWidth() / 2); double layoutY = tile.getLocation().getLayoutY(CELL_SIZE) - (tile.getMinHeight() / 2); tile.setLayoutX(layoutX); tile.setLayoutY(layoutY); gridGroup.getChildren().add(tile); } public Tile addRandomTile(Location randomLocation) { var tile = Tile.newRandomTile(); tile.setLocation(randomLocation); double layoutX = tile.getLocation().getLayoutX(CELL_SIZE) - (tile.getMinWidth() / 2); double layoutY = tile.getLocation().getLayoutY(CELL_SIZE) - (tile.getMinHeight() / 2); tile.setLayoutX(layoutX); tile.setLayoutY(layoutY); tile.setScaleX(0); tile.setScaleY(0); gridGroup.getChildren().add(tile); return tile; } public void startGame() { restoreRecord(); time = LocalTime.now(); timer.playFromStart(); } public void setPoints(int points) { gameMovePoints.set(points); } public int getPoints() { return gameMovePoints.get(); } public void addPoints(int points) { gameMovePoints.set(gameMovePoints.get() + points); gameScoreProperty.set(gameScoreProperty.get() + points); } public void setGameOver(boolean gameOver) { gameOverProperty.set(gameOver); } public void setGameWin(boolean won) { if (!gameWonProperty.get()) { gameWonProperty.set(won); } } public void pauseGame() { if (!gamePauseProperty.get()) { gamePauseProperty.set(true); } } public void aboutGame() { if (!gameAboutProperty.get()) { gameAboutProperty.set(true); } } public void quitGame() { if (gameQuitProperty.get()) { exitGame(); } else { gameQuitProperty.set(true); } } protected BooleanProperty isLayerOn() { return layerOnProperty; } protected BooleanProperty resetGameProperty() { return resetGame; } protected BooleanProperty clearGameProperty() { return clearGame; } protected BooleanProperty saveGameProperty() { return saveGame; } protected BooleanProperty restoreGameProperty() { return restoreGame; } public boolean saveSession() { if (!gameSaveProperty.get()) { gameSaveProperty.set(true); } return true; } /* * Once we have confirmation */ public void saveSession(Map<Location, Tile> gameGrid) { saveGame.set(false); sessionManager.saveSession(gameGrid, gameScoreProperty.getValue(), LocalTime.now().minusNanos(time.toNanoOfDay()).toNanoOfDay()); keepGoing(); } public boolean restoreSession() { if (!gameRestoreProperty.get()) { gameRestoreProperty.set(true); } return true; } /* * Once we have confirmation */ public boolean restoreSession(Map<Location, Tile> gameGrid) { restoreGame.set(false); doClearGame(); timer.stop(); var sTime = new SimpleStringProperty(""); int score = sessionManager.restoreSession(gameGrid, sTime); if (score >= 0) { gameScoreProperty.set(score); // check tiles>=2048 gameWonProperty.set(false); gameGrid.forEach((l, t) -> { if (t != null && t.getValue() >= GameManager.FINAL_VALUE_TO_WIN) { gameWonProperty.removeListener(wonListener); gameWonProperty.set(true); gameWonProperty.addListener(wonListener); } }); if (!sTime.get().isEmpty()) { time = LocalTime.now().minusNanos(Long.parseLong(sTime.get())); } timer.play(); return true; } // not session found, restart again doResetGame(); return false; } public void saveRecord() { var recordManager = new RecordManager(gridOperator.getGridSize()); recordManager.saveRecord(gameScoreProperty.getValue()); } private void restoreRecord() { var recordManager = new RecordManager(gridOperator.getGridSize()); gameBestProperty.set(recordManager.restoreRecord()); } public void removeTiles(Set<Tile> mergedToBeRemoved) { gridGroup.getChildren().removeAll(mergedToBeRemoved); } }