mergedToBeRemoved = new HashSet<>();
35 |
36 | private final Board board;
37 | private final GridOperator gridOperator;
38 | private Animation shakingAnimation;
39 |
40 | public GameManager() {
41 | this(UserSettings.LOCAL.getGridSize());
42 | }
43 |
44 | /**
45 | * GameManager is a Group containing a Board that holds a grid and the score a
46 | * Map holds the location of the tiles in the grid
47 | *
48 | * The purpose of the game is sum the value of the tiles up to 2048 points Based
49 | * on the Javascript version: ...
50 | *
51 | * @param gridSize defines the size of the grid, default 6x6
52 | */
53 | public GameManager(int gridSize) {
54 | this.gameGrid = new HashMap<>();
55 |
56 | gridOperator = new GridOperator(gridSize);
57 | board = new Board(gridOperator);
58 | board.setToolBar(createToolBar());
59 | this.getChildren().add(board);
60 |
61 | var trueProperty = new SimpleBooleanProperty(true);
62 | board.clearGameProperty().and(trueProperty).addListener((ov, b1, b2) -> initializeGameGrid());
63 | board.resetGameProperty().and(trueProperty).addListener((ov, b1, b2) -> startGame());
64 | board.restoreGameProperty().and(trueProperty).addListener((ov, b1, b2) -> doRestoreSession());
65 | board.saveGameProperty().and(trueProperty).addListener((ov, b1, b2) -> doSaveSession());
66 |
67 | initializeGameGrid();
68 | startGame();
69 | }
70 |
71 | /**
72 | * Initializes all cells in gameGrid map to null
73 | */
74 | private void initializeGameGrid() {
75 | gameGrid.clear();
76 | locations.clear();
77 | gridOperator.traverseGrid((x, y) -> {
78 | var location = new Location(x, y);
79 | locations.add(location);
80 | gameGrid.put(location, null);
81 | return 0;
82 | });
83 | }
84 |
85 | /**
86 | * Starts the game by adding 1 or 2 tiles at random locations
87 | */
88 | private void startGame() {
89 | var tile0 = Tile.newRandomTile();
90 | var randomLocs = new ArrayList<>(locations);
91 | Collections.shuffle(randomLocs);
92 | var locs = randomLocs.stream().limit(2).iterator();
93 | tile0.setLocation(locs.next());
94 |
95 | Tile tile1 = null;
96 | if (new Random().nextFloat() <= 0.8) { // gives 80% chance to add a second tile
97 | tile1 = Tile.newRandomTile();
98 | if (tile1.getValue() == 4 && tile0.getValue() == 4) {
99 | tile1 = Tile.newTile(2);
100 | }
101 | tile1.setLocation(locs.next());
102 | }
103 |
104 | Stream.of(tile0, tile1).filter(Objects::nonNull).forEach(t -> gameGrid.put(t.getLocation(), t));
105 |
106 | redrawTilesInGameGrid();
107 |
108 | board.startGame();
109 | }
110 |
111 | /**
112 | * Redraws all tiles in the gameGrid
object
113 | */
114 | private void redrawTilesInGameGrid() {
115 | gameGrid.values().stream().filter(Objects::nonNull).forEach(board::addTile);
116 | }
117 |
118 | /**
119 | * Moves the tiles according to given direction At any move, takes care of merge
120 | * tiles, add a new one and perform the required animations It updates the score
121 | * and checks if the user won the game or if the game is over
122 | *
123 | * @param direction is the selected direction to move the tiles
124 | */
125 | private void moveTiles(Direction direction) {
126 | synchronized (gameGrid) {
127 | if (movingTiles) {
128 | return;
129 | }
130 | }
131 |
132 | board.setPoints(0);
133 | mergedToBeRemoved.clear();
134 | var parallelTransition = new ParallelTransition();
135 | gridOperator.sortGrid(direction);
136 | final int tilesWereMoved = gridOperator.traverseGrid((x, y) -> {
137 | var currentLocation = new Location(x, y);
138 | var farthestLocation = findFarthestLocation(currentLocation, direction); // farthest available location
139 | var opTile = optionalTile(currentLocation);
140 |
141 | var result = new AtomicInteger();
142 | var nextLocation = farthestLocation.offset(direction); // calculates to a possible merge
143 |
144 | optionalTile(nextLocation).filter(t -> t.isMergeable(opTile) && !t.isMerged()).ifPresent(t -> {
145 | var tile = opTile.get();
146 | t.merge(tile);
147 | t.toFront();
148 | gameGrid.put(nextLocation, t);
149 | gameGrid.replace(currentLocation, null);
150 |
151 | parallelTransition.getChildren().add(animateExistingTile(tile, t.getLocation()));
152 | parallelTransition.getChildren().add(animateMergedTile(t));
153 | mergedToBeRemoved.add(tile);
154 |
155 | board.addPoints(t.getValue());
156 |
157 | if (t.getValue() == FINAL_VALUE_TO_WIN) {
158 | board.setGameWin(true);
159 | }
160 | result.set(1);
161 | });
162 |
163 | if (result.get() == 0 && opTile.isPresent() && !farthestLocation.equals(currentLocation)) {
164 | var tile = opTile.get();
165 | parallelTransition.getChildren().add(animateExistingTile(tile, farthestLocation));
166 |
167 | gameGrid.put(farthestLocation, tile);
168 | gameGrid.replace(currentLocation, null);
169 |
170 | tile.setLocation(farthestLocation);
171 |
172 | result.set(1);
173 | }
174 |
175 | return result.get();
176 | });
177 |
178 | board.animateScore();
179 | if (parallelTransition.getChildren().size() > 0) {
180 | parallelTransition.setOnFinished(e -> {
181 | board.removeTiles(mergedToBeRemoved);
182 | // reset merged after each movement
183 | gameGrid.values().stream().filter(Objects::nonNull).forEach(Tile::clearMerge);
184 |
185 | var randomAvailableLocation = findRandomAvailableLocation();
186 | if (randomAvailableLocation.isEmpty() && mergeMovementsAvailable() == 0) {
187 | // game is over if there are no more moves available
188 | board.setGameOver(true);
189 | } else if (randomAvailableLocation.isPresent() && tilesWereMoved > 0) {
190 | synchronized (gameGrid) {
191 | movingTiles = false;
192 | }
193 | addAndAnimateRandomTile(randomAvailableLocation.get());
194 | }
195 | });
196 |
197 | synchronized (gameGrid) {
198 | movingTiles = true;
199 | }
200 |
201 | parallelTransition.play();
202 | }
203 |
204 | if (tilesWereMoved == 0) {
205 | // no tiles got moved
206 | // shake the game pane
207 | if (shakingAnimation == null) {
208 | shakingAnimation = createShakeGamePaneAnimation();
209 | }
210 |
211 | if (!shakingAnimationPlaying) {
212 | shakingAnimation.play();
213 | shakingAnimationPlaying = true;
214 | }
215 | }
216 | }
217 |
218 | private boolean shakingAnimationPlaying = false;
219 | private boolean shakingXYState = false;
220 |
221 | /**
222 | * optionalTile allows using tiles from the map at some location, whether they
223 | * are null or not
224 | *
225 | * @param loc location of the tile
226 | * @return an Optional containing null or a valid tile
227 | */
228 | private Optional optionalTile(Location loc) {
229 | return Optional.ofNullable(gameGrid.get(loc));
230 | }
231 |
232 | /**
233 | * Searches for the farthest empty location where the current tile could go
234 | *
235 | * @param location of the tile
236 | * @param direction of movement
237 | * @return a location
238 | */
239 | private Location findFarthestLocation(Location location, Direction direction) {
240 | Location farthest;
241 |
242 | do {
243 | farthest = location;
244 | location = farthest.offset(direction);
245 | } while (gridOperator.isValidLocation(location) && optionalTile(location).isEmpty());
246 |
247 | return farthest;
248 | }
249 |
250 | /**
251 | * Finds the number of pairs of tiles that can be merged
252 | *
253 | * This method is called only when the grid is full of tiles, what makes the use
254 | * of Optional unnecessary, but it could be used when the board is not full to
255 | * find the number of pairs of merge-able tiles and provide a hint for the user,
256 | * for instance
257 | *
258 | * @return the number of pairs of tiles that can be merged
259 | */
260 | private int mergeMovementsAvailable() {
261 | final var pairsOfMergeableTiles = new AtomicInteger();
262 |
263 | Stream.of(Direction.UP, Direction.LEFT).parallel().forEach(direction -> gridOperator.traverseGrid((x, y) -> {
264 | var thisLocation = new Location(x, y);
265 | optionalTile(thisLocation).ifPresent(t -> {
266 | if (t.isMergeable(optionalTile(thisLocation.offset(direction)))) {
267 | pairsOfMergeableTiles.incrementAndGet();
268 | }
269 | });
270 | return 0;
271 | }));
272 | return pairsOfMergeableTiles.get();
273 | }
274 |
275 | /**
276 | * Finds a random location or returns null if none exist
277 | *
278 | * @return a random location or null
if there are no more locations
279 | * available
280 | */
281 | private Optional findRandomAvailableLocation() {
282 | var availableLocations = locations.stream().filter(l -> gameGrid.get(l) == null).collect(Collectors.toList());
283 |
284 | if (availableLocations.isEmpty()) {
285 | return Optional.empty();
286 | }
287 |
288 | Collections.shuffle(availableLocations);
289 |
290 | // returns a random location
291 | return Optional.of(availableLocations.get(new Random().nextInt(availableLocations.size())));
292 | }
293 |
294 | /**
295 | * Adds a tile of random value to a random location with a proper animation
296 | *
297 | */
298 | private void addAndAnimateRandomTile(Location randomLocation) {
299 | var tile = board.addRandomTile(randomLocation);
300 | gameGrid.put(tile.getLocation(), tile);
301 |
302 | animateNewlyAddedTile(tile).play();
303 | }
304 |
305 | /**
306 | * Animation that creates a fade in effect when a tile is added to the game by
307 | * increasing the tile scale from 0 to 100%
308 | *
309 | * @param tile to be animated
310 | * @return a scale transition
311 | */
312 | private ScaleTransition animateNewlyAddedTile(Tile tile) {
313 | final var scaleTransition = new ScaleTransition(ANIMATION_NEWLY_ADDED_TILE, tile);
314 | scaleTransition.setToX(1.0);
315 | scaleTransition.setToY(1.0);
316 | scaleTransition.setInterpolator(Interpolator.EASE_OUT);
317 | scaleTransition.setOnFinished(e -> {
318 | // after last movement on full grid, check if there are movements available
319 | if (this.gameGrid.values().parallelStream().noneMatch(Objects::isNull) && mergeMovementsAvailable() == 0) {
320 | board.setGameOver(true);
321 | }
322 | });
323 | return scaleTransition;
324 | }
325 |
326 | private Animation createShakeGamePaneAnimation() {
327 | var shakingAnimation = new Timeline(new KeyFrame(Duration.seconds(0.05), (ae) -> {
328 | var parent = getParent();
329 |
330 | if (shakingXYState) {
331 | parent.setLayoutX(parent.getLayoutX() + 5);
332 | parent.setLayoutY(parent.getLayoutY() + 5);
333 | } else {
334 | parent.setLayoutX(parent.getLayoutX() - 5);
335 | parent.setLayoutY(parent.getLayoutY() - 5);
336 | }
337 |
338 | shakingXYState = !shakingXYState;
339 | }));
340 |
341 | shakingAnimation.setCycleCount(6);
342 | shakingAnimation.setAutoReverse(false);
343 | shakingAnimation.setOnFinished(event -> {
344 | shakingXYState = false;
345 | shakingAnimationPlaying = false;
346 | });
347 |
348 | return shakingAnimation;
349 | }
350 |
351 | /**
352 | * Animation that moves the tile from its previous location to a new location
353 | *
354 | * @param tile to be animated
355 | * @param newLocation new location of the tile
356 | * @return a timeline
357 | */
358 | private Timeline animateExistingTile(Tile tile, Location newLocation) {
359 | var timeline = new Timeline();
360 | var kvX = new KeyValue(tile.layoutXProperty(),
361 | newLocation.getLayoutX(Board.CELL_SIZE) - (tile.getMinHeight() / 2), Interpolator.EASE_OUT);
362 | var kvY = new KeyValue(tile.layoutYProperty(),
363 | newLocation.getLayoutY(Board.CELL_SIZE) - (tile.getMinHeight() / 2), Interpolator.EASE_OUT);
364 |
365 | var kfX = new KeyFrame(ANIMATION_EXISTING_TILE, kvX);
366 | var kfY = new KeyFrame(ANIMATION_EXISTING_TILE, kvY);
367 |
368 | timeline.getKeyFrames().add(kfX);
369 | timeline.getKeyFrames().add(kfY);
370 |
371 | return timeline;
372 | }
373 |
374 | /**
375 | * Animation that creates a pop effect when two tiles merge by increasing the
376 | * tile scale to 120% at the middle, and then going back to 100%
377 | *
378 | * @param tile to be animated
379 | * @return a sequential transition
380 | */
381 | private SequentialTransition animateMergedTile(Tile tile) {
382 | final var scale0 = new ScaleTransition(ANIMATION_MERGED_TILE, tile);
383 | scale0.setToX(1.2);
384 | scale0.setToY(1.2);
385 | scale0.setInterpolator(Interpolator.EASE_IN);
386 |
387 | final var scale1 = new ScaleTransition(ANIMATION_MERGED_TILE, tile);
388 | scale1.setToX(1.0);
389 | scale1.setToY(1.0);
390 | scale1.setInterpolator(Interpolator.EASE_OUT);
391 |
392 | return new SequentialTransition(scale0, scale1);
393 | }
394 |
395 | /**
396 | * Move the tiles according user input if overlay is not on
397 | *
398 | */
399 | public void move(Direction direction) {
400 | if (!board.isLayerOn().get()) {
401 | moveTiles(direction);
402 | }
403 | }
404 |
405 | /**
406 | * Set gameManager scale to adjust overall game size
407 | *
408 | */
409 | public void setScale(double scale) {
410 | this.setScaleX(scale);
411 | this.setScaleY(scale);
412 | }
413 |
414 | /**
415 | * Pauses the game time, covers the grid
416 | */
417 | public void pauseGame() {
418 | board.pauseGame();
419 | }
420 |
421 | /**
422 | * Quit the game with confirmation
423 | */
424 | public void quitGame() {
425 | board.quitGame();
426 | }
427 |
428 | /**
429 | * Ask to save the game from a properties file with confirmation
430 | */
431 | public void saveSession() {
432 | board.saveSession();
433 | }
434 |
435 | /**
436 | * Save the game to a properties file, without confirmation
437 | */
438 | private void doSaveSession() {
439 | board.saveSession(gameGrid);
440 | }
441 |
442 | /**
443 | * Ask to restore the game from a properties file with confirmation
444 | */
445 | public void restoreSession() {
446 | board.restoreSession();
447 | }
448 |
449 | /**
450 | * Restore the game from a properties file, without confirmation
451 | */
452 | private void doRestoreSession() {
453 | initializeGameGrid();
454 | if (board.restoreSession(gameGrid)) {
455 | redrawTilesInGameGrid();
456 | }
457 | }
458 |
459 | /**
460 | * Save actual record to a properties file
461 | */
462 | public void saveRecord() {
463 | board.saveRecord();
464 | }
465 |
466 | private HBox createToolBar() {
467 | var btItem1 = createButtonItem("mSave", "Save Session", t -> saveSession());
468 | var btItem2 = createButtonItem("mRestore", "Restore Session", t -> restoreSession());
469 | var btItem3 = createButtonItem("mPause", "Pause Game", t -> board.pauseGame());
470 | var btItem4 = createButtonItem("mReplay", "Try Again", t -> board.showTryAgainOverlay());
471 | var btItem5 = createButtonItem("mInfo", "About the Game", t -> board.aboutGame());
472 | var btItem6 = createButtonItem("mQuit", "Quit Game", t -> quitGame());
473 |
474 | var toolbar = new HBox(btItem1, btItem2, btItem3, btItem4, btItem5, btItem6);
475 | toolbar.setAlignment(Pos.CENTER);
476 | toolbar.setPadding(new Insets(10.0));
477 | return toolbar;
478 | }
479 |
480 | private Button createButtonItem(String symbol, String text, EventHandler t) {
481 | var g = new Button();
482 | g.setPrefSize(40, 40);
483 | g.setId(symbol);
484 | g.setOnAction(t);
485 | g.setTooltip(new Tooltip(text));
486 | return g;
487 | }
488 |
489 | }
490 |
--------------------------------------------------------------------------------
/src/main/java/io/fxgame/game2048/GamePane.java:
--------------------------------------------------------------------------------
1 | package io.fxgame.game2048;
2 |
3 | import javafx.beans.property.BooleanProperty;
4 | import javafx.beans.property.SimpleBooleanProperty;
5 | import javafx.beans.value.ChangeListener;
6 | import javafx.geometry.Bounds;
7 | import javafx.stage.Stage;
8 | import javafx.scene.layout.StackPane;
9 | import javafx.scene.text.Font;
10 |
11 | import java.util.Objects;
12 |
13 | import static io.fxgame.game2048.Direction.*;
14 |
15 | /**
16 | * @author Narendra Yusuf
17 | */
18 | public class GamePane extends StackPane {
19 |
20 | private final GameManager gameManager;
21 | private final Bounds gameBounds;
22 |
23 | static {
24 | // Downloaded from https://01.org/clear-sans/blogs
25 | // The font may be used and redistributed under the terms of the Apache License 2.0
26 | Font.loadFont(Objects.requireNonNull(Game2048.class.getResource("ClearSans-Bold.ttf")).toExternalForm(), 10.0);
27 | }
28 |
29 | public GamePane() {
30 | gameManager = new GameManager(UserSettings.LOCAL.getGridSize());
31 | gameBounds = gameManager.getLayoutBounds();
32 |
33 | getChildren().add(gameManager);
34 |
35 | getStyleClass().addAll("game-root");
36 | ChangeListener resize = (ov, v, v1) -> {
37 | double scale = Math.min((getWidth() - UserSettings.MARGIN) / gameBounds.getWidth(),
38 | (getHeight() - UserSettings.MARGIN) / gameBounds.getHeight());
39 | gameManager.setScale(scale);
40 | gameManager.setLayoutX((getWidth() - gameBounds.getWidth()) / 2d);
41 | gameManager.setLayoutY((getHeight() - gameBounds.getHeight()) / 2d);
42 | };
43 | widthProperty().addListener(resize);
44 | heightProperty().addListener(resize);
45 |
46 | addKeyHandlers();
47 | addSwipeHandlers();
48 | setFocusTraversable(true);
49 | setOnMouseClicked(e -> requestFocus());
50 | }
51 |
52 | private final BooleanProperty cmdCtrlKeyPressed = new SimpleBooleanProperty(false);
53 |
54 | private void addKeyHandlers() {
55 | setOnKeyPressed(ke -> {
56 | var keyCode = ke.getCode();
57 | switch (keyCode) {
58 | case CONTROL, COMMAND -> cmdCtrlKeyPressed.set(true);
59 | case S -> gameManager.saveSession();
60 | case R -> gameManager.restoreSession();
61 | case P -> gameManager.pauseGame();
62 | case Q -> {
63 | if (!cmdCtrlKeyPressed.get()) gameManager.quitGame();
64 | }
65 | case F -> {
66 | var stage = ((Stage) getScene().getWindow());
67 | stage.setFullScreen(!stage.isFullScreen());
68 | }
69 | default -> {
70 | if (keyCode.isArrowKey()) move(Direction.valueFor(keyCode));
71 | }
72 | }
73 | });
74 |
75 | setOnKeyReleased(ke -> {
76 | var keyCode = ke.getCode();
77 | switch (keyCode) {
78 | case CONTROL, COMMAND -> cmdCtrlKeyPressed.set(false);
79 | }
80 | });
81 | }
82 |
83 | private void addSwipeHandlers() {
84 | setOnSwipeUp(e -> move(UP));
85 | setOnSwipeRight(e -> move(RIGHT));
86 | setOnSwipeLeft(e -> move(LEFT));
87 | setOnSwipeDown(e -> move(DOWN));
88 | }
89 |
90 | private void move(Direction direction) {
91 | gameManager.move(direction);
92 | }
93 |
94 | public GameManager getGameManager() {
95 | return gameManager;
96 | }
97 |
98 | }
99 |
--------------------------------------------------------------------------------
/src/main/java/io/fxgame/game2048/GameState.java:
--------------------------------------------------------------------------------
1 | package io.fxgame.game2048;
2 |
3 | import javafx.beans.property.BooleanProperty;
4 | import javafx.beans.property.IntegerProperty;
5 | import javafx.beans.property.SimpleBooleanProperty;
6 | import javafx.beans.property.SimpleIntegerProperty;
7 |
8 | import java.util.Arrays;
9 |
10 | class GameState {
11 |
12 | final IntegerProperty gameScoreProperty = new SimpleIntegerProperty(0);
13 | final IntegerProperty gameBestProperty = new SimpleIntegerProperty(0);
14 | final IntegerProperty gameMovePoints = new SimpleIntegerProperty(0);
15 | final BooleanProperty gameWonProperty = new SimpleBooleanProperty(false);
16 | final BooleanProperty gameOverProperty = new SimpleBooleanProperty(false);
17 | final BooleanProperty gameAboutProperty = new SimpleBooleanProperty(false);
18 | final BooleanProperty gamePauseProperty = new SimpleBooleanProperty(false);
19 | final BooleanProperty gameTryAgainProperty = new SimpleBooleanProperty(false);
20 | final BooleanProperty gameSaveProperty = new SimpleBooleanProperty(false);
21 | final BooleanProperty gameRestoreProperty = new SimpleBooleanProperty(false);
22 | final BooleanProperty gameQuitProperty = new SimpleBooleanProperty(false);
23 | final BooleanProperty layerOnProperty = new SimpleBooleanProperty(false);
24 | final BooleanProperty resetGame = new SimpleBooleanProperty(false);
25 | final BooleanProperty clearGame = new SimpleBooleanProperty(false);
26 | final BooleanProperty restoreGame = new SimpleBooleanProperty(false);
27 | final BooleanProperty saveGame = new SimpleBooleanProperty(false);
28 |
29 | public void keepGoing() {
30 | layerOnProperty.set(false);
31 | gamePauseProperty.set(false);
32 | gameTryAgainProperty.set(false);
33 | gameSaveProperty.set(false);
34 | gameRestoreProperty.set(false);
35 | gameAboutProperty.set(false);
36 | gameQuitProperty.set(false);
37 | }
38 |
39 | public void clearState() {
40 | Arrays.asList(clearGame, resetGame, restoreGame, saveGame, layerOnProperty, gameWonProperty, gameOverProperty,
41 | gameAboutProperty, gamePauseProperty, gameTryAgainProperty, gameSaveProperty, gameRestoreProperty,
42 | gameQuitProperty).forEach(a -> a.set(false));
43 |
44 | gameScoreProperty.set(0);
45 |
46 | clearGame.set(true);
47 | }
48 |
49 | public void resetGame() {
50 | resetGame.set(true);
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/main/java/io/fxgame/game2048/GridOperator.java:
--------------------------------------------------------------------------------
1 | package io.fxgame.game2048;
2 |
3 | import java.util.Collections;
4 | import java.util.List;
5 | import java.util.concurrent.atomic.AtomicInteger;
6 | import java.util.function.IntBinaryOperator;
7 | import java.util.stream.Collectors;
8 | import java.util.stream.IntStream;
9 |
10 | /**
11 | * @author Narendra Yusuf
12 | */
13 | public class GridOperator {
14 |
15 | public static final int DEFAULT_GRID_SIZE = 6; // 6x6
16 | public static final int MIN_GRID_SIZE = 4;
17 | public static final int MAX_GRID_SIZE = 16;
18 |
19 | private final int gridSize;
20 | private final List traversalX;
21 | private final List traversalY;
22 |
23 | public GridOperator() {
24 | this(DEFAULT_GRID_SIZE);
25 | }
26 |
27 | public GridOperator(int gridSize) {
28 | if (gridSize < MIN_GRID_SIZE || gridSize > MAX_GRID_SIZE) {
29 | throw new IllegalArgumentException(String.format("Grid size must be of range %s and %s.", MIN_GRID_SIZE, MAX_GRID_SIZE));
30 | }
31 |
32 | this.gridSize = gridSize;
33 | this.traversalX = IntStream.range(0, gridSize).boxed().collect(Collectors.toList());
34 | this.traversalY = IntStream.range(0, gridSize).boxed().collect(Collectors.toList());
35 | }
36 |
37 | public void sortGrid(Direction direction) {
38 | Collections.sort(traversalX, direction.equals(Direction.RIGHT) ? Collections.reverseOrder() : Integer::compareTo);
39 | Collections.sort(traversalY, direction.equals(Direction.DOWN) ? Collections.reverseOrder() : Integer::compareTo);
40 | //traversalX.sort(direction.equals(Direction.RIGHT) ? Collections.reverseOrder() : Integer::compareTo);
41 | //traversalY.sort(direction.equals(Direction.DOWN) ? Collections.reverseOrder() : Integer::compareTo);
42 | }
43 |
44 | public int traverseGrid(IntBinaryOperator func) {
45 | var at = new AtomicInteger();
46 | traversalX.forEach(t_x -> traversalY.forEach(t_y -> at.addAndGet(func.applyAsInt(t_x, t_y))));
47 |
48 | return at.get();
49 | }
50 |
51 | public int getGridSize() {
52 | return gridSize;
53 | }
54 |
55 | public boolean isValidLocation(Location loc) {
56 | return loc.isValidFor(gridSize);
57 | }
58 |
59 | }
60 |
--------------------------------------------------------------------------------
/src/main/java/io/fxgame/game2048/Location.java:
--------------------------------------------------------------------------------
1 | package io.fxgame.game2048;
2 |
3 | /**
4 | * @author Narendra Yusuf
5 | */
6 | public record Location(int x, int y) {
7 |
8 | public Location offset(Direction direction) {
9 | return new Location(x + direction.getX(), y + direction.getY());
10 | }
11 |
12 | @Override
13 | public String toString() {
14 | return "Location{" + "x=" + x + ", y=" + y + '}';
15 | }
16 |
17 | public double getLayoutY(int CELL_SIZE) {
18 | if (y == 0) {
19 | return CELL_SIZE / 2;
20 | }
21 | return (y * CELL_SIZE) + CELL_SIZE / 2;
22 | }
23 |
24 | public double getLayoutX(int CELL_SIZE) {
25 | if (x == 0) {
26 | return CELL_SIZE / 2;
27 | }
28 | return (x * CELL_SIZE) + CELL_SIZE / 2;
29 | }
30 |
31 | public boolean isValidFor(int gridSize) {
32 | return x >= 0 && x < gridSize && y >= 0 && y < gridSize;
33 | }
34 |
35 | }
36 |
--------------------------------------------------------------------------------
/src/main/java/io/fxgame/game2048/RecordManager.java:
--------------------------------------------------------------------------------
1 | package io.fxgame.game2048;
2 |
3 | import java.util.Properties;
4 |
5 | /**
6 | * @author Narendra Yusuf
7 | */
8 | public class RecordManager {
9 |
10 | public final String propertiesFilename;
11 | private final Properties props = new Properties();
12 |
13 | public RecordManager(int grid_size) {
14 | this.propertiesFilename = "game2048_" + grid_size + "_record.properties";
15 | }
16 |
17 | public void saveRecord(Integer score) {
18 | int oldRecord = restoreRecord();
19 | props.setProperty("record", Integer.toString(Math.max(oldRecord, score)));
20 | UserSettings.LOCAL.store(props, propertiesFilename);
21 | }
22 |
23 | public int restoreRecord() {
24 | UserSettings.LOCAL.restore(props, propertiesFilename);
25 |
26 | String score = props.getProperty("record");
27 | if (score != null) {
28 | return Integer.parseInt(score);
29 | }
30 | return 0;
31 | }
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/src/main/java/io/fxgame/game2048/SessionManager.java:
--------------------------------------------------------------------------------
1 | package io.fxgame.game2048;
2 |
3 | import javafx.beans.property.StringProperty;
4 |
5 | import java.util.Map;
6 | import java.util.Properties;
7 |
8 | /**
9 | * @author Narendra Yusuf
10 | */
11 | public class SessionManager {
12 |
13 | public final String propertiesFilename;
14 | private final Properties props = new Properties();
15 | private final GridOperator gridOperator;
16 |
17 | public SessionManager(GridOperator gridOperator) {
18 | this.gridOperator = gridOperator;
19 | this.propertiesFilename = "game2048_" + gridOperator.getGridSize() + ".properties";
20 | }
21 |
22 | protected void saveSession(Map gameGrid, Integer score, Long time) {
23 | gridOperator.traverseGrid((x, y) -> {
24 | var tile = gameGrid.get(new Location(x, y));
25 | props.setProperty("Location_" + x + "_" + y, tile != null ? tile.getValue().toString() : "0");
26 | return 0;
27 | });
28 | props.setProperty("score", score.toString());
29 | props.setProperty("time", time.toString());
30 | UserSettings.LOCAL.store(props, propertiesFilename);
31 | }
32 |
33 | protected int restoreSession(Map gameGrid, StringProperty time) {
34 | UserSettings.LOCAL.restore(props, propertiesFilename);
35 |
36 | gridOperator.traverseGrid((x, y) -> {
37 | var val = props.getProperty("Location_" + x + "_" + y);
38 | if (!val.equals("0")) {
39 | var tile = Tile.newTile(Integer.parseInt(val));
40 | var location = new Location(x, y);
41 | tile.setLocation(location);
42 | gameGrid.put(location, tile);
43 | }
44 | return 0;
45 | });
46 |
47 | time.set(props.getProperty("time"));
48 |
49 | var score = props.getProperty("score");
50 | if (score != null) {
51 | return Integer.parseInt(score);
52 | }
53 | return 0;
54 | }
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/src/main/java/io/fxgame/game2048/Tile.java:
--------------------------------------------------------------------------------
1 | package io.fxgame.game2048;
2 |
3 | import javafx.geometry.Pos;
4 | import javafx.scene.control.Label;
5 | import java.util.Optional;
6 | import java.util.Random;
7 |
8 | /**
9 | * @author Narendra Yusuf
10 | */
11 | public class Tile extends Label {
12 |
13 | private Integer value;
14 | private Location location;
15 | private Boolean merged;
16 |
17 | private static final Random random = new Random();
18 |
19 | public static Tile newRandomTile() {
20 | int value = random.nextDouble() < 0.9 ? 2 : 4;
21 | return new Tile(value);
22 | }
23 |
24 | public static Tile newTile(int value) {
25 | if (value % 2 != 0) {
26 | throw new IllegalArgumentException("Tile value must be multiple of 2");
27 | }
28 |
29 | return new Tile(value);
30 | }
31 |
32 | private Tile(Integer value) {
33 | final int squareSize = Board.CELL_SIZE - 13;
34 | setMinSize(squareSize, squareSize);
35 | setMaxSize(squareSize, squareSize);
36 | setPrefSize(squareSize, squareSize);
37 | setAlignment(Pos.CENTER);
38 |
39 | this.value = value;
40 | this.merged = false;
41 | setText(value.toString());
42 | getStyleClass().addAll("game-label", "game-tile-" + value);
43 | }
44 |
45 | public void merge(Tile another) {
46 | getStyleClass().remove("game-tile-" + value);
47 | this.value += another.getValue();
48 | setText(value.toString());
49 | merged = true;
50 | getStyleClass().add("game-tile-" + value);
51 | }
52 |
53 | public Integer getValue() {
54 | return value;
55 | }
56 |
57 | public Location getLocation() {
58 | return location;
59 | }
60 |
61 | public void setLocation(Location location) {
62 | this.location = location;
63 | }
64 |
65 | @Override
66 | public String toString() {
67 | return "Tile{" + "value=" + value + ", location=" + location + '}';
68 | }
69 |
70 | public boolean isMerged() {
71 | return merged;
72 | }
73 |
74 | public void clearMerge() {
75 | merged = false;
76 | }
77 |
78 | public boolean isMergeable(Optional anotherTile) {
79 | return anotherTile.filter(t -> t.getValue().equals(getValue())).isPresent();
80 | }
81 | }
82 |
--------------------------------------------------------------------------------
/src/main/java/io/fxgame/game2048/UserSettings.java:
--------------------------------------------------------------------------------
1 | package io.fxgame.game2048;
2 |
3 | import java.io.*;
4 | import java.nio.file.Files;
5 | import java.nio.file.Path;
6 | import java.util.Properties;
7 | import java.util.logging.Level;
8 | import java.util.logging.Logger;
9 |
10 | /**
11 | * UserSettings
12 | * @author Narendra Yusuf
13 | */
14 | public enum UserSettings {
15 |
16 | LOCAL;
17 |
18 | public final static int MARGIN = 36;
19 | private final File userGameFolder;
20 |
21 | UserSettings() {
22 | var userHome = System.getProperty("user.home");
23 | var gamePath = Path.of(userHome, ".fx2048");
24 | gamePath.toFile().mkdir();
25 | userGameFolder = gamePath.toFile();
26 |
27 | try {
28 | var isWindows = System.getProperty("os.arch").toUpperCase().contains("WINDOWS");
29 | if (isWindows) {
30 | Files.setAttribute(gamePath, "dos:hidden", true);
31 | }
32 | } catch (IOException e) {
33 | e.printStackTrace();
34 | }
35 | }
36 |
37 | public void store(Properties data, String fileName) {
38 | try {
39 | data.store(new FileWriter(new File(userGameFolder, fileName)), fileName);
40 | } catch (IOException e) {
41 | e.printStackTrace();
42 | }
43 | }
44 |
45 | public void restore(Properties props, String fileName) {
46 | try (var reader = new FileReader(new File(userGameFolder, fileName))) {
47 | props.load(reader);
48 | } catch (FileNotFoundException e) {
49 | Logger.getLogger(UserSettings.class.getName()).log(Level.INFO, "Previous game record not found.");
50 | } catch (IOException ex) {
51 | Logger.getLogger(UserSettings.class.getName()).log(Level.SEVERE, null, ex);
52 | }
53 | }
54 |
55 | public int getGridSize() {
56 | // @TODO save settings for grid size
57 | return GridOperator.DEFAULT_GRID_SIZE;
58 | }
59 |
60 | }
61 |
--------------------------------------------------------------------------------
/src/main/java/module-info.java:
--------------------------------------------------------------------------------
1 | module fxgame {
2 | requires java.logging;
3 |
4 | requires javafx.base;
5 | requires javafx.controls;
6 | requires javafx.graphics;
7 |
8 | exports io.fxgame.game2048;
9 | }
10 |
--------------------------------------------------------------------------------
/src/main/resources/io/fxgame/game2048/ClearSans-Bold.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/NarendraYSF/JavaFX2048/4edeac8eb7107f2990842cc59a77565ccd9485f5/src/main/resources/io/fxgame/game2048/ClearSans-Bold.ttf
--------------------------------------------------------------------------------
/src/main/resources/io/fxgame/game2048/game.css:
--------------------------------------------------------------------------------
1 | .game-root {
2 | -fx-background-color: #faf8ef;
3 | }
4 | .game-vbox {
5 | -fx-background-color: #bbada0;
6 | -fx-padding: 5 15 5 15;
7 | -fx-background-radius: 3;
8 | -fx-border-radius: 3;
9 | }
10 | .game-titScore {
11 | -fx-font-size: 13px;
12 | -fx-text-fill: #eee4da;
13 | }
14 | .game-score {
15 | -fx-font-size: 25px;
16 | -fx-text-fill: white;
17 | }
18 | .game-points {
19 | -fx-font-size: 25px;
20 | -fx-text-fill: rgba(119, 110, 101, 0.9);
21 | }
22 | .game-time {
23 | -fx-font-size: 14px;
24 | -fx-text-fill: #bbada0;
25 | }
26 | .game-backGrid {
27 | -fx-background-color: #bbada0;
28 | -fx-border-color: #bbada0;
29 | -fx-border-width: 2;
30 | -fx-background-radius: 6;
31 | -fx-border-radius: 6;
32 | }
33 | .game-grid {
34 | -fx-background-color: #bbada0;
35 | -fx-background-radius: 3;
36 | -fx-border-radius: 3;
37 | }
38 | .game-grid-cell {
39 | -fx-fill: #cdc1b4;
40 | -fx-stroke-width: 14px;
41 | -fx-stroke-type: centered;
42 | -fx-stroke: #BBADA0;
43 | }
44 |
45 | .game-label {
46 | -fx-font-smoothing-type: lcd;
47 | -fx-smooth: true;
48 | -fx-font-family: 'Clear Sans Bold';
49 | }
50 |
51 | .game-title {
52 | -fx-font-size: 80px;
53 | -fx-text-fill: #776e65;
54 | }
55 | .game-subtitle {
56 | -fx-font-size: 40px;
57 | -fx-text-fill: #f2b179; // f9f6f2
58 | -fx-effect: dropshadow( three-pass-box, rgba(243, 215, 116, 1), 10, 0.5, 0, 0 );
59 | }
60 |
61 | .game-overlay {
62 | -fx-font-size: 60px;
63 | -fx-text-fill: #f9f6f2;
64 | -fx-border-color: #f9f6f2;
65 | -fx-border-width: 2;
66 | -fx-background-radius: 6;
67 | -fx-border-radius: 6;
68 | }
69 |
70 | .game-overlay-pause, .game-overlay-quit {
71 | -fx-opacity: 0.9;
72 | -fx-background-color: #f9f6f2;
73 | }
74 | .game-lblPause, .game-lblOver, .game-lblQuit {
75 | -fx-font-size: 60px;
76 | -fx-text-fill: #776e65;
77 | }
78 | .game-lblWarning {
79 | -fx-font-size: 18px;
80 | -fx-font-style: italic;
81 | -fx-text-fill: #f65e3b;
82 | }
83 | .game-lblAbout {
84 | -fx-font-size: 50px;
85 | -fx-fill: #776e65;
86 | }
87 | .game-lblAbout2 {
88 | -fx-font-size: 25px;
89 | -fx-fill: #f2b179;
90 | -fx-effect: dropshadow( three-pass-box, rgba(243, 215, 116, 1), 10, 0.5, 0, 0 );
91 | -fx-translate-y: -25;
92 | }
93 | .game-lblAboutSub {
94 | -fx-font-size: 25px;
95 | -fx-text-fill: #cdc1b4;
96 | }
97 | .game-lblAboutSub2 {
98 | -fx-font-size: 25px;
99 | -fx-text-fill: derive(#cdc1b4,-40%);
100 | }
101 | .game-overlay-won {
102 | -fx-background-color: rgba(237, 194, 46, 0.5);
103 | }
104 | .game-lblWon {
105 | -fx-font-size: 60px;
106 | -fx-text-fill: #f9f6f2;
107 | }
108 | .game-overlay-over {
109 | -fx-opacity: 0.6;
110 | -fx-background-color: #f9f6f2;
111 | }
112 | .game-button {
113 | -fx-font-size: 30px;
114 | -fx-text-fill: white;
115 | -fx-background-color: #8f7a66;
116 | -fx-border-color: #8f7a66;
117 | -fx-border-width: 2;
118 | -fx-background-radius: 3;
119 | -fx-border-radius: 3;
120 | -fx-padding: 6 20 12 20;
121 | }
122 | .game-button:pressed {
123 | -fx-padding: 4 22 12 22;
124 | }
125 | .game-button:focused, .game-button:hover {
126 | -fx-border-color: -fx-focus-color;
127 | -fx-border-width: 0.5px;
128 | }
129 | .game-tile-2 {
130 | -fx-font-size: 55px;
131 | -fx-text-fill: #776e65;
132 | -fx-background-color: #eee4da;
133 | -fx-background-radius: 3;
134 | -fx-border-radius: 3;
135 | }
136 | .game-tile-4 {
137 | -fx-font-size: 55px;
138 | -fx-text-fill: #776e65;
139 | -fx-background-color: #ede0c8;
140 | -fx-background-radius: 3;
141 | -fx-border-radius: 3;
142 | }
143 | .game-tile-8 {
144 | -fx-font-size: 55px;
145 | -fx-text-fill: #f9f6f2;
146 | -fx-background-color: #f2b179;
147 | -fx-background-radius: 3;
148 | -fx-border-radius: 3;
149 | }
150 | .game-tile-16 {
151 | -fx-font-size: 55px;
152 | -fx-text-fill: #f9f6f2;
153 | -fx-background-color: #f59563;
154 | -fx-background-radius: 3;
155 | -fx-border-radius: 3;
156 | }
157 | .game-tile-32 {
158 | -fx-font-size: 55px;
159 | -fx-text-fill: #f9f6f2;
160 | -fx-background-color: #f67c5f;
161 | -fx-background-radius: 3;
162 | -fx-border-radius: 3;
163 | }
164 | .game-tile-64 {
165 | -fx-font-size: 55px;
166 | -fx-text-fill: #f9f6f2;
167 | -fx-background-color: #f65e3b;
168 | -fx-background-radius: 3;
169 | -fx-border-radius: 3;
170 | }
171 | .game-tile-128 {
172 | -fx-font-size: 45px;
173 | -fx-text-fill: #f9f6f2;
174 | -fx-background-color: #edcf72;
175 | -fx-background-radius: 3;
176 | -fx-border-radius: 3;
177 | -fx-effect: dropshadow( three-pass-box, rgba(243, 215, 116, 0.2381), 30, 0.5, 0, 0 );
178 | -fx-border-color: rgba(255, 255, 255, 0.14286);
179 | -fx-border-width: 1;
180 | }
181 | .game-tile-256 {
182 | -fx-font-size: 45px;
183 | -fx-text-fill: #f9f6f2;
184 | -fx-background-color: #edcc61;
185 | -fx-background-radius: 3;
186 | -fx-border-radius: 3;
187 | -fx-effect: dropshadow( three-pass-box, rgba(243, 215, 116, 0.31746), 30, 0.5, 0, 0 );
188 | -fx-border-color: rgba(255, 255, 255, 0.19048);
189 | -fx-border-width: 1;
190 | }
191 | .game-tile-512 {
192 | -fx-font-size: 45px;
193 | -fx-text-fill: #f9f6f2;
194 | -fx-background-color: #edc850;
195 | -fx-background-radius: 3;
196 | -fx-border-radius: 3;
197 | -fx-effect: dropshadow( three-pass-box, rgba(243, 215, 116, 0.39683), 30, 0.5, 0, 0 );
198 | -fx-border-color: rgba(255, 255, 255, 0.2381);
199 | -fx-border-width: 1;
200 | }
201 | .game-tile-1024 {
202 | -fx-font-size: 35px;
203 | -fx-text-fill: #f9f6f2;
204 | -fx-background-color: #edc53f;
205 | -fx-background-radius: 3;
206 | -fx-border-radius: 3;
207 | -fx-effect: dropshadow( three-pass-box, rgba(243, 215, 116, 0.47619), 30, 0.5, 0, 0 );
208 | -fx-border-color: rgba(255, 255, 255, 0.28571);
209 | -fx-border-width: 1;
210 | }
211 | .game-tile-2048 {
212 | -fx-font-size: 35px;
213 | -fx-text-fill: #f9f6f2;
214 | -fx-background-color: #edc22e;
215 | -fx-background-radius: 3;
216 | -fx-border-radius: 3;
217 | -fx-effect: dropshadow( three-pass-box, rgba(243, 215, 116, 0.55556), 30, 0.5, 0, 0 );
218 | -fx-border-color: rgba(255, 255, 255, 0.33333);
219 | -fx-border-width: 1;
220 | }
221 | .game-tile-4096 {
222 | -fx-font-size: 30px;
223 | -fx-text-fill: #f9f6f2;
224 | -fx-background-color: #b885ac;
225 | -fx-background-radius: 3;
226 | -fx-border-radius: 3;
227 | }
228 | .game-tile-8192 {
229 | -fx-font-size: 30px;
230 | -fx-text-fill: #f9f6f2;
231 | -fx-background-color: #af6da9;
232 | -fx-background-radius: 3;
233 | -fx-border-radius: 3;
234 | }
235 | .game-tile-16384 {
236 | -fx-font-size: 30px;
237 | -fx-text-fill: #f9f6f2;
238 | -fx-background-color: #ab61a7;
239 | -fx-background-radius: 3;
240 | -fx-border-radius: 3;
241 | }
242 | .game-tile-32768 {
243 | -fx-font-size: 30px;
244 | -fx-text-fill: #f9f6f2;
245 | -fx-background-color: #a755a6;
246 | -fx-background-radius: 3;
247 | -fx-border-radius: 3;
248 | }
249 | .game-tile-65536, .game-tile-131072, .game-tile-262144 {
250 | -fx-font-size: 25px;
251 | -fx-text-fill: #f9f6f2;
252 | -fx-background-color: #3c3a32;
253 | -fx-background-radius: 3;
254 | -fx-border-radius: 3;
255 | }
256 |
257 | /*
258 | SVG ICONS https://icomoon.io/app/
259 | */
260 |
261 | #mSave {
262 | -fx-shape: "M16 18l8-8h-6v-8h-4v8h-6zM23.273 14.727l-2.242 2.242 8.128 3.031-13.158 4.907-13.158-4.907 8.127-3.031-2.242-2.242-8.727 3.273v8l16 6 16-6v-8z";
263 | -fx-background-color: #f9f6f2;
264 | -fx-scale-x: 1.0;
265 | -fx-scale-y: 1.0;
266 | -fx-focus-traversable: true;
267 | }
268 | #mSave:hover {
269 | -fx-effect: dropshadow(gaussian,derive(#776e65,-20%),10,0.5,0,0);
270 | }
271 | #mSave:pressed {
272 | -fx-translate-y: 2px;
273 | -fx-effect: dropshadow(gaussian,derive(-fx-focus-color,-20%),10,0.5,0,0);
274 | }
275 | #mRestore {
276 | -fx-shape: "M0 28h32v2h-32zM32 24v2h-32v-2l4-8h8v4h8v-4h8zM7 10l9-9 9 9h-7v8h-4v-8z";
277 | -fx-background-color: #f9f6f2;
278 | -fx-scale-x: 1.0;
279 | -fx-scale-y: 1.0;
280 | -fx-focus-traversable: true;
281 | }
282 | #mRestore:hover {
283 | -fx-effect: dropshadow(gaussian,derive(#776e65,-20%),10,0.5,0,0);
284 | }
285 | #mRestore:pressed {
286 | -fx-translate-y: 2px;
287 | -fx-effect: dropshadow(gaussian,derive(-fx-focus-color,-20%),10,0.5,0,0);
288 | }
289 | #mPause {
290 | -fx-shape: "M16 0c-8.837 0-16 7.163-16 16s7.163 16 16 16 16-7.163 16-16-7.163-16-16-16zM16 29c-7.18 0-13-5.82-13-13s5.82-13 13-13 13 5.82 13 13-5.82 13-13 13zM10 10h4v12h-4zM18 10h4v12h-4z";
291 | -fx-background-color: #f9f6f2;
292 | -fx-scale-x: 1.1;
293 | -fx-scale-y: 1.1;
294 | -fx-focus-traversable: true;
295 | }
296 | #mPause:hover {
297 | -fx-effect: dropshadow(gaussian,derive(#776e65,-20%),10,0.5,0,0);
298 | }
299 | #mPause:pressed {
300 | -fx-translate-y: 2px;
301 | -fx-effect: dropshadow(gaussian,derive(-fx-focus-color,-20%),10,0.5,0,0);
302 | }
303 | #mReplay {
304 | -fx-shape: "M0 27.429v-8q0-0.464 0.339-0.804t0.804-0.339h8q0.464 0 0.804 0.339t0.339 0.804-0.339 0.804l-2.446 2.446q1.268 1.179 2.875 1.821t3.339 0.643q2.393 0 4.464-1.161t3.321-3.196q0.196-0.304 0.946-2.089 0.143-0.411 0.536-0.411h3.429q0.232 0 0.402 0.17t0.17 0.402q0 0.089-0.018 0.125-1.143 4.786-4.786 7.759t-8.536 2.973q-2.607 0-5.045-0.982t-4.348-2.804l-2.304 2.304q-0.339 0.339-0.804 0.339t-0.804-0.339-0.339-0.804zM0.321 13.143v-0.125q1.161-4.786 4.821-7.759t8.571-2.973q2.607 0 5.071 0.991t4.375 2.795l2.321-2.304q0.339-0.339 0.804-0.339t0.804 0.339 0.339 0.804v8q0 0.464-0.339 0.804t-0.804 0.339h-8q-0.464 0-0.804-0.339t-0.339-0.804 0.339-0.804l2.464-2.464q-2.643-2.446-6.232-2.446-2.393 0-4.464 1.161t-3.321 3.196q-0.196 0.304-0.946 2.089-0.143 0.411-0.536 0.411h-3.554q-0.232 0-0.402-0.17t-0.17-0.402z";
305 | -fx-background-color: #f9f6f2;
306 | -fx-scale-x: 1.0;
307 | -fx-scale-y: 1.0;
308 | -fx-focus-traversable: true;
309 | }
310 | #mReplay:hover {
311 | -fx-effect: dropshadow(gaussian,derive(#776e65,-20%),10,0.5,0,0);
312 | }
313 | #mReplay:pressed {
314 | -fx-translate-y: 2px;
315 | -fx-effect: dropshadow(gaussian,derive(-fx-focus-color,-20%),10,0.5,0,0);
316 | }
317 | #mInfo {
318 | -fx-shape: "M1344 1472v128q0 26-19 45t-45 19h-512q-26 0-45-19t-19-45v-128q0-26 19-45t45-19h64v-384h-64q-26 0-45-19t-19-45v-128q0-26 19-45t45-19h384q26 0 45 19t19 45v576h64q26 0 45 19t19 45zm-128-1152v192q0 26-19 45t-45 19h-256q-26 0-45-19t-19-45v-192q0-26 19-45t45-19h256q26 0 45 19t19 45z";
319 | -fx-background-color: #f9f6f2;
320 | -fx-scale-x: 0.7;
321 | -fx-scale-y: 1.0;
322 | -fx-focus-traversable: true;
323 | }
324 | #mInfo:hover {
325 | -fx-effect: dropshadow(gaussian,derive(#776e65,-20%),10,0.5,0,0);
326 | }
327 | #mInfo:pressed {
328 | -fx-translate-y: 2px;
329 | -fx-effect: dropshadow(gaussian,derive(-fx-focus-color,-20%),10,0.5,0,0);
330 | }
331 | #mQuit {
332 | -fx-shape: "M20 4.581v4.249c1.131 0.494 2.172 1.2 3.071 2.099 1.889 1.889 2.929 4.4 2.929 7.071s-1.040 5.182-2.929 7.071c-1.889 1.889-4.4 2.929-7.071 2.929s-5.182-1.040-7.071-2.929c-1.889-1.889-2.929-4.4-2.929-7.071s1.040-5.182 2.929-7.071c0.899-0.899 1.94-1.606 3.071-2.099v-4.249c-5.783 1.721-10 7.077-10 13.419 0 7.732 6.268 14 14 14s14-6.268 14-14c0-6.342-4.217-11.698-10-13.419zM14 0h4v16h-4z";
333 | -fx-background-color: #f9f6f2;
334 | -fx-scale-x: 1.0;
335 | -fx-scale-y: 1.0;
336 | -fx-focus-traversable: true;
337 | }
338 | #mQuit:hover {
339 | -fx-effect: dropshadow(gaussian,derive(#776e65,-20%),10,0.5,0,0);
340 | }
341 | #mQuit:pressed {
342 | -fx-translate-y: 2px;
343 | -fx-effect: dropshadow(gaussian,derive(-fx-focus-color,-20%),10,0.5,0,0);
344 | }
--------------------------------------------------------------------------------