/* * Copyright (C) 2013-2015 2048FX * Jose Pereda, Bruno Borges & Jens Deters * All rights reserved. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see <http://www.gnu.org/licenses/>. */ package org.jpereda.game2048; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Random; import java.util.Set; import java.util.concurrent.atomic.AtomicInteger; import javafx.animation.Interpolator; import javafx.animation.KeyFrame; import javafx.animation.KeyValue; import javafx.animation.ParallelTransition; import javafx.animation.ScaleTransition; import javafx.animation.SequentialTransition; import javafx.animation.Timeline; import javafx.beans.property.BooleanProperty; import javafx.beans.property.SimpleBooleanProperty; import javafx.scene.Group; import javafx.scene.layout.HBox; import javafx.util.Duration; /** * * @author jpereda */ public class GameManager extends Group { public static final int FINAL_VALUE_TO_WIN = 2048; private Board board; private GridOperator gridOperator; private volatile boolean movingTiles = false; private final List<Location> locations = new ArrayList<>(); private Map<Location, Tile> gameGrid; private final Set<Tile> mergedToBeRemoved = new HashSet<>(); private final BooleanProperty moving = new SimpleBooleanProperty(false); private final ParallelTransition parallelTransition = new ParallelTransition(); public GameManager() { this(GridOperator.DEFAULT_GRID_SIZE); } /** * GameManager is a Group containing a Board that holds a grid and the score * a Map holds the location of the tiles in the grid * * The purpose of the game is sum the value of the tiles up to 2048 points * Based on the Javascript version: https://github.com/gabrielecirulli/2048 * * @param gridSize defines the size of the grid, default 4x4 */ public GameManager(int gridSize) { this.gameGrid = new HashMap<>(); gridOperator=new GridOperator(gridSize); board = new Board(gridOperator); this.getChildren().add(board); board.clearGameProperty().addListener((ov, b, b1) -> { if (b1) { initializeGameGrid(); } }); board.resetGameProperty().addListener((ov, b, b1) -> { if (b1) { startGame(); } }); board.restoreGameProperty().addListener((ov, b, b1) -> { if (b1) { doRestoreSession(); } }); board.saveGameProperty().addListener((ov, b, b1) -> { if (b1) { doSaveSession(); } }); initializeGameGrid(); startGame(); } /** * Initializes all cells in gameGrid map to null */ private void initializeGameGrid() { gameGrid.clear(); locations.clear(); for(Integer x: gridOperator.getTraverseX()){ for(Integer y:gridOperator.getTraverseY()){ Location thisloc = new Location(x, y); locations.add(thisloc); gameGrid.put(thisloc, null); } } } /** * Starts the game by adding 1 or 2 tiles at random locations */ private void startGame() { Tile tile0 = Tile.newRandomTile(); List<Location> randomLocs = new ArrayList<>(locations); Collections.shuffle(randomLocs); tile0.setLocation(randomLocs.get(0)); gameGrid.put(tile0.getLocation(), tile0); if (new Random().nextFloat() <= 0.8) { // gives 80% chance to add a second tile Tile tile1 = Tile.newRandomTile(); if (tile1.getValue() == 4 && tile0.getValue() == 4) { tile1 = Tile.newTile(2); } tile1.setLocation(randomLocs.get(1)); gameGrid.put(tile1.getLocation(), tile1); } redrawTilesInGameGrid(); board.startGame(); } /** * Redraws all tiles in the <code>gameGrid</code> object */ private void redrawTilesInGameGrid() { for(Tile t: gameGrid.values()){ if(t!=null){ board.addTile(t); } } } private int tilesWereMoved = 0; private void moveTiles(Direction direction) { synchronized (gameGrid) { if (movingTiles) { return; } } board.setPoints(0); tilesWereMoved = 0; gridOperator.sortGrid(direction); for(Integer x: gridOperator.getTraverseX()){ for(Integer y:gridOperator.getTraverseY()){ Location thisloc = new Location(x, y); Location farthestLocation = findFarthestLocation(thisloc, direction); // farthest available location Tile tile = gameGrid.get(thisloc); AtomicInteger result=new AtomicInteger(); Location nextLocation = farthestLocation.offset(direction); // calculates to a possible merge if(nextLocation!=null && gameGrid.get(nextLocation)!=null){ Tile t=gameGrid.get(nextLocation); if(t.isMergeable(tile) && !t.isMerged()){ t.merge(tile); t.toFront(); gameGrid.put(nextLocation, t); gameGrid.put(thisloc, null); parallelTransition.getChildren().add(animateExistingTile(tile, t.getLocation())); parallelTransition.getChildren().add(animateMergedTile(t)); mergedToBeRemoved.add(tile); board.addPoints(t.getValue()); if (t.getValue() == FINAL_VALUE_TO_WIN) { board.setGameWin(true); } result.set(1); } } if (result.get()==0 && tile!=null && !farthestLocation.equals(thisloc)) { parallelTransition.getChildren().add(animateExistingTile(tile, farthestLocation)); gameGrid.put(farthestLocation, tile); gameGrid.put(thisloc, null); tile.setLocation(farthestLocation); result.set(1); } tilesWereMoved+=result.get(); } } board.animateScore(); parallelTransition.setOnFinished(e -> { synchronized (gameGrid) { movingTiles = false; moving.set(false); } board.getGridGroup().getChildren().removeAll(mergedToBeRemoved); Location randomAvailableLocation = findRandomAvailableLocation(); if (randomAvailableLocation == null && mergeMovementsAvailable() == 0 ) { // game is over if there are no more moves available board.setGameOver(true); } else if (randomAvailableLocation != null && tilesWereMoved > 0) { addAndAnimateRandomTile(randomAvailableLocation); } mergedToBeRemoved.clear(); // reset merged after each movement for(Tile t:gameGrid.values()){ if(t!=null){ t.clearMerge(); } } }); synchronized (gameGrid) { movingTiles = true; moving.set(true); } parallelTransition.play(); parallelTransition.getChildren().clear(); } /** * Searchs for the farthest empty location where the current tile could go * @param location of the tile * @param direction of movement * @return a location */ private Location findFarthestLocation(Location location, Direction direction) { Location farthest; do { farthest = location; location = farthest.offset(direction); } while (gridOperator.isValidLocation(location) && gameGrid.get(location)==null); return farthest; } /** * Finds the number of pairs of tiles that can be merged * * This method is called only when the grid is full of tiles, * what makes the use of Optional unnecessary, but it could be used when the * board is not full to find the number of pairs of mergeable tiles and provide a hint * for the user, for instance * @return the number of pairs of tiles that can be merged */ private int mergeMovementsAvailable() { final AtomicInteger pairsOfMergeableTiles = new AtomicInteger(); int cont=0; Direction direction= Direction.UP; do { for(Integer x: gridOperator.getTraverseX()){ for(Integer y:gridOperator.getTraverseY()){ Location thisloc = new Location(x, y); Tile tile = gameGrid.get(thisloc); if(tile!=null){ Tile offsetTile = gameGrid.get(thisloc.offset(direction)); if(offsetTile!=null && tile.isMergeable(offsetTile)){ pairsOfMergeableTiles.incrementAndGet(); } } } } direction = Direction.LEFT; cont++; } while(cont<2); return pairsOfMergeableTiles.get(); } /** * Finds a random location or returns null if none exist * * @return a random location or <code>null</code> if there are no more * locations available */ private Location findRandomAvailableLocation() { List<Location> availableLocations = new ArrayList<>(); for(Integer x: gridOperator.getTraverseX()){ for(Integer y:gridOperator.getTraverseY()){ Location thisloc = new Location(x, y); if(gameGrid.get(thisloc)==null){ availableLocations.add(thisloc); } } } if (availableLocations.isEmpty()) { return null; } Collections.shuffle(availableLocations); Location randomLocation = availableLocations.get(new Random().nextInt(availableLocations.size())); return randomLocation; } /** * Adds a tile of random value to a random location with a proper animation * * @param randomLocation */ private void addAndAnimateRandomTile(Location randomLocation) { Tile tile = board.addRandomTile(randomLocation); gameGrid.put(tile.getLocation(), tile); animateNewlyAddedTile(tile).play(); } /** * Animation that creates a fade in effect when a tile is added to the game * by increasing the tile scale from 0 to 100% * @param tile to be animated * @return a scale transition */ private ScaleTransition animateNewlyAddedTile(Tile tile) { final ScaleTransition scaleTransition = new ScaleTransition(Duration.millis(125), tile); scaleTransition.setToX(1.0); scaleTransition.setToY(1.0); scaleTransition.setInterpolator(Interpolator.EASE_OUT); scaleTransition.setOnFinished(e -> { // after last movement on full grid, check if there are movements available if (checkEndGame()) { board.setGameOver(true); } }); return scaleTransition; } /** * Animation that moves the tile from its previous location to a new location * @param tile to be animated * @param newLocation new location of the tile * @return a timeline */ private Timeline animateExistingTile(Tile tile, Location newLocation) { Timeline timeline = new Timeline(); KeyValue kvX = new KeyValue(tile.layoutXProperty(), newLocation.getLayoutX(Board.CELL_SIZE) - (tile.getMinHeight() / 2), Interpolator.EASE_OUT); KeyValue kvY = new KeyValue(tile.layoutYProperty(), newLocation.getLayoutY(Board.CELL_SIZE) - (tile.getMinHeight() / 2), Interpolator.EASE_OUT); KeyFrame kfX = new KeyFrame(Duration.millis(65), kvX); KeyFrame kfY = new KeyFrame(Duration.millis(65), kvY); timeline.getKeyFrames().add(kfX); timeline.getKeyFrames().add(kfY); return timeline; } /** * Animation that creates a pop effect when two tiles merge * by increasing the tile scale to 120% at the middle, and then going back to 100% * @param tile to be animated * @return a sequential transition */ private SequentialTransition animateMergedTile(Tile tile) { final ScaleTransition scale0 = new ScaleTransition(Duration.millis(80), tile); scale0.setToX(1.2); scale0.setToY(1.2); scale0.setInterpolator(Interpolator.EASE_IN); final ScaleTransition scale1 = new ScaleTransition(Duration.millis(80), tile); scale1.setToX(1.0); scale1.setToY(1.0); scale1.setInterpolator(Interpolator.EASE_OUT); return new SequentialTransition(scale0, scale1); } private boolean checkEndGame(){ int result=0; for(Integer x: gridOperator.getTraverseX()){ for(Integer y:gridOperator.getTraverseY()){ if(gameGrid.get(new Location(y, x))==null){ result+=1; } } } return result==0 && mergeMovementsAvailable() == 0; } /*************************************************************************/ /************************ Public methods *********************************/ /*************************************************************************/ /** * Move the tiles according user input if overlay is not on * @param direction */ public void move(Direction direction){ if (!board.isLayerOn().get()) { moveTiles(direction); } } /** * Set gameManager scale to adjust overall game size * @param scale */ public void setScale(double scale) { this.setScaleX(scale); this.setScaleY(scale); } /** * Check if overlay covers the grid or not * @return */ public BooleanProperty isLayerOn() { return board.isLayerOn(); } /** * Pauses the game time, covers the grid */ public void pauseGame() { board.pauseGame(); } /** * Quit the game with confirmation */ public void quitGame() { board.quitGame(); } /** * Ask to save the game from a properties file with confirmation */ public void saveSession() { board.saveSession(); } /** * Save the game to a properties file, without confirmation */ private void doSaveSession() { board.saveSession(gameGrid); } /** * Ask to restore the game from a properties file with confirmation */ public void restoreSession() { board.restoreSession(); } /** * Restore the game from a properties file, without confirmation */ private void doRestoreSession() { initializeGameGrid(); if (board.restoreSession(gameGrid)) { redrawTilesInGameGrid(); } } /** * Save actual record to a properties file */ public void saveRecord() { board.saveRecord(); } public void tryAgain() { board.tryAgain(); } public void aboutGame() { board.aboutGame(); } public void setToolBar(HBox toolbar){ board.setToolBar(toolbar); } public void externalPause(boolean b, boolean b1){ board.externalPause(b,b1); } }