5
\$\begingroup\$

I am a beginner at programming. I made this snake game in JavaFX to practice and improve my coding skills. I tried to make my code clean, but I'm not sure if it is, or if it is well-organized. I also tried to make the design object-oriented. So I would like to ask your opinion on my code's structure and clarity. I would appreciate any advice you give me to improve.

This is what the game looks like:

Snake game running

When the game is over, a replay button appears

I divided the program into four classes:

  1. SnakeGame is the main class that is responsible for putting the game together and starting, restarting, ending the game, and taking user input.

  2. GameBoard extends Pane. It is responsible for the snake and the fruit objects. It has an inner class, SnakeMovement, that extends TimerTask. This task invokes the methods for checking for collisions, checking if the snake ate a fruit, and invokes the moveSnake() method in the Snake class.

  3. Snake class extends ArrayList<Rectangle>. It has the methods for creating the snake, moving the snake, and adding a new head to the snake when a fruit is eaten.

  4. Fruit class extends Rectangle. It has the methods to get a random empty tile and to spawn the fruit in an empty tile.

SnakeGame.java

import java.util.Timer;
import java.util.TimerTask;
import javafx.application.Application;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.Region;
import javafx.stage.Stage;


public class SnakeGame extends Application {

    private BorderPane root;
    
    private HBox scoreBar;
    private Label scoreIndicator;
    private int score;
    private Button replay;
    
    private GameBoard GAME_BOARD;
    private final int TILE_SIZE = 20;
    
    private int keyX;
    private int keyY;
    
    private Timer timer;
    private TimerTask task;
    
    @Override
    public void start(Stage stage) {
        root = new BorderPane();
        root.setId("root");
        
        scoreBar = new HBox();
        scoreBar.setId("scoreBar");
        root.setTop(scoreBar);
        
        scoreIndicator = new Label("Score: 0");
        scoreIndicator.setId("scoreIndicator");
        
        replay = new Button("Replay");
        replay.setId("replay");
        replay.setOnAction(e -> restartGame());
        
        Region filler = new Region();
        HBox.setHgrow(filler, Priority.ALWAYS);
        
        scoreBar.getChildren().addAll(scoreIndicator, filler, replay);
        
        GAME_BOARD = new GameBoard(this, TILE_SIZE, 20, 20);
        GAME_BOARD.setOnKeyPressed(e -> takeInput(e));
        root.setCenter(GAME_BOARD);
        
        Scene scene = new Scene(root);
        String css = this.getClass().getResource("snake_game.css").toExternalForm();
        scene.getStylesheets().add(css);
        
        stage.setScene(scene);
        stage.setTitle("Snake Game");
        stage.setResizable(false);
        stage.setOnCloseRequest(e -> {
            System.exit(0);
        });
        stage.show();
        
        startGame();
    }
    
    public void startGame() {
        GAME_BOARD.requestFocus();
        replay.setVisible(false);
        timer = new Timer();
        task = GAME_BOARD.new SnakeMovement();
        timer.scheduleAtFixedRate(task, 100, 100);
    }
    
    public void endGame() {
        timer.cancel();
        replay.setVisible(true);
    }
    
    public void restartGame() {
        resetKeys();
        resetScore();
        GAME_BOARD.resetGameBoard();
        startGame();
    }
    
    public void resetKeys() {
        keyX = 0;
        keyY = 0;
    }
    
    public void resetScore() {
        score = 0;
        scoreIndicator.setText("Score: 0");
    }
    
    public void increaseScore() {
        score++;
        scoreIndicator.setText("Score: " + score);
    }
    
    public void takeInput(KeyEvent e) {
        switch (e.getCode()) {
            case DOWN -> { 
                keyX = 0; keyY = TILE_SIZE;
            }
            case UP -> { 
                keyX = 0; keyY = -TILE_SIZE;
            }
            case RIGHT -> { 
                keyX = TILE_SIZE; keyY = 0;
            }
            case LEFT -> { 
                keyX = -TILE_SIZE; keyY = 0;
            }
        }
    }
     
    public int getKeyX() {
        return keyX;
    }
    
    public int getKeyY() {
        return keyY;
    }
    
    public static void main(String[] args) {
        launch(args);
    }

}

GameBoard.java

import java.util.ArrayList;
import java.util.TimerTask;
import javafx.application.Platform;
import javafx.geometry.Point2D;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;


public class GameBoard extends Pane {
    
    private final SnakeGame GAME;
    
    private final int TILE_SIZE;
    private final int TILES_IN_ROW;
    private final int TILES_IN_COLUMN;
    private final int ROW_SIZE;
    private final int COLUMN_SIZE;
    private final int TOTAL_TILES;
    
    private ArrayList<Point2D> emptyTiles;
    
    private final Snake SNAKE;
    private final Fruit FRUIT;
    
    private int dX;
    private int dY;
    
    public GameBoard(SnakeGame game, int tileSize, int tilesInRow, int tilesInColumn) {
        
        GAME = game;
        
        TILE_SIZE = tileSize;
        TILES_IN_ROW = tilesInRow;
        TILES_IN_COLUMN = tilesInColumn;
        
        ROW_SIZE = TILE_SIZE * TILES_IN_ROW;
        COLUMN_SIZE = TILE_SIZE * TILES_IN_COLUMN;
        TOTAL_TILES = TILES_IN_ROW * TILES_IN_COLUMN;
        
        setPrefSize(ROW_SIZE, COLUMN_SIZE);
        
        SNAKE = new Snake(this, TILE_SIZE, 3, Color.GREENYELLOW);
        FRUIT = new Fruit(TILE_SIZE, Color.RED);
        getChildren().add(FRUIT);
        
        setGameBoard();
    }
    
    public void setGameBoard() {
        dY = TILE_SIZE;
        emptyTiles = createEmptyTilesList();
        SNAKE.setEmptyTiles(emptyTiles);
        FRUIT.setEmptyTiles(emptyTiles);
        SNAKE.generateSnake();
        FRUIT.spawnFruit();
    }
    
    public void resetGameBoard() {
        dX = 0;
        dY = 0;
        getChildren().removeAll(SNAKE);
        SNAKE.clear();
        setGameBoard();
    }
    
    public ArrayList<Point2D> createEmptyTilesList() {
        ArrayList<Point2D> emptyTiles = new ArrayList<>();
        for (int tile = 1, x = 0, y = 0; tile <= TOTAL_TILES; tile++) {
            emptyTiles.add(new Point2D(x, y));
            x += TILE_SIZE;
            if (tile % TILES_IN_ROW == 0) {
                x = 0;
                y += TILE_SIZE;
            }
        }
        return emptyTiles;
    }
    
    public boolean isDirectionValid() {
        //the snake shouldn't move in the opposite direction
        return GAME.getKeyX() + dX != 0 && GAME.getKeyY() + dY != 0;
    }
    
    public boolean ateFruit() {
       Rectangle head = SNAKE.getSnakeHead();
       return head.getX() == FRUIT.getX() && head.getY() == FRUIT.getY();
    }
        
    public boolean willHitWall() {
        Rectangle head = SNAKE.getSnakeHead();
        return head.getX() + dX < 0 || head.getX() + dX >= this.getWidth() || 
                head.getY() + dY < 0 || head.getY() + dY >= this.getHeight();
    }
    
    public boolean willHitBody() {
        Rectangle head = SNAKE.getSnakeHead();
        for (Rectangle body : SNAKE) {
            if (head.getX() + dX == body.getX() && head.getY() + dY == body.getY())
                return true;
        }
        return false;
    }
    
    class SnakeMovement extends TimerTask {

        @Override
        public void run() {
            Platform.runLater(() -> {
                
                if (isDirectionValid()) {
                    dX = GAME.getKeyX();
                    dY = GAME.getKeyY();
                }
                
                if (ateFruit()) {
                    FRUIT.spawnFruit();
                    SNAKE.addHead();
                    GAME.increaseScore();
                }
                
                if (willHitWall() || willHitBody()) {
                    GAME.endGame();
                    return;
                }
        
                SNAKE.moveSnake(dX, dY);
            });
        }
    }
    
}

Snake.java

    import java.util.ArrayList;
    import javafx.geometry.Point2D;
    import javafx.scene.paint.Color;
    import javafx.scene.shape.Rectangle;
    
    
    public class Snake extends ArrayList<Rectangle> {
        
        private final int SNAKE_BLOCK_SIZE;
        private final int SNAKE_INITIAL_LENGTH;
        private final Color SNAKE_COLOR;
        
        private final GameBoard GAME_BOARD;
        private ArrayList<Point2D> emptyTiles;
        
        public Snake(GameBoard gameBoard, int snakeBlockSize, int snakeInitialLength, Color snakeColor) {
            GAME_BOARD = gameBoard;
            SNAKE_BLOCK_SIZE = snakeBlockSize;
            SNAKE_INITIAL_LENGTH = snakeInitialLength;
            SNAKE_COLOR = snakeColor;
        }
        
        public Rectangle createBodyPart() {
            Rectangle body = new Rectangle(SNAKE_BLOCK_SIZE, SNAKE_BLOCK_SIZE, SNAKE_COLOR);
            GAME_BOARD.getChildren().add(body);
            return body;
        }
        
        public void generateSnake() {
            for (int tile = SNAKE_INITIAL_LENGTH - 1; tile >= 0; tile--) {
                Rectangle body = createBodyPart();
                int startX = (int)GAME_BOARD.getPrefWidth() / 2;
                //0 is the head and 2 is the tail, if snake's initial length is 3,
                //startY will be 40, 20, and 0, for elements 0, 1, and 2 respectively
                //since the snake is moving downward
                int startY = tile * SNAKE_BLOCK_SIZE;
                body.setX(startX);
                body.setY(startY);
                emptyTiles.remove(new Point2D(startX, startY));
                add(body);
            }
        }
        
        public void moveSnake(int dX, int dY) {
            
            Rectangle head = getSnakeHead();
            
            boolean isHeadAdded = head.getX() == get(1).getX() && head.getY() == get(1).getY();
            
            double oldX = head.getX();
            double oldY = head.getY();
            
            double newX = oldX + dX;
            double newY = oldY + dY;
            
            head.setX(newX);
            head.setY(newY);
            //the head now occupies this tile
            emptyTiles.remove(new Point2D(newX, newY));
            
            //when a new head is added, the rest of the body stays still for one task
            //so there is no need to go through the loop, and the tail's place shouldn't
            //be added to emptyTiles
            if (isHeadAdded) {
                return;
            }
            
            for (int i = 1; i < size(); i++) {
                Rectangle body = get(i);
                
                double currentX = body.getX();
                double currentY = body.getY();
                
                body.setX(oldX);
                body.setY(oldY);
                
                oldX = currentX;
                oldY = currentY;
            }
            //the old tile of the tail is now empty
            emptyTiles.add(new Point2D(oldX, oldY));
            
        }
        
        public void addHead() {
            Rectangle oldHead = getSnakeHead();
            Rectangle newHead = createBodyPart();
            
            //the new head is set on the same tile as the old head initially
            //but it will be moved in the moveSnake method afterwards
            newHead.setX(oldHead.getX());
            newHead.setY(oldHead.getY());
            
            setSnakeHead(newHead);
        }
        
        public Rectangle getSnakeHead() {
            return get(0);
        }
        
        public void setSnakeHead(Rectangle newHead) {
            add(0, newHead);
        }
        
        public void setEmptyTiles(ArrayList<Point2D> emptyTiles) {
            this.emptyTiles = emptyTiles;
        }
    }

Fruit.java

import java.util.ArrayList;
import java.util.Random;
import javafx.geometry.Point2D;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;

public class Fruit extends Rectangle {
    
    private ArrayList<Point2D> emptyTiles;
    
    public Fruit(int blockSize, Color color) {
        super(blockSize, blockSize, color);
    }
    
    public Point2D generateRandomTile() {
        Random random = new Random();
        int randomIndex = random.nextInt(emptyTiles.size());
        return emptyTiles.get(randomIndex);
    }
    
    public void spawnFruit() {
        Point2D randomTile = generateRandomTile();
        this.setX(randomTile.getX());
        this.setY(randomTile.getY());
    }
    
    public void setEmptyTiles(ArrayList<Point2D> emptyTiles) {
        this.emptyTiles = emptyTiles;
    }
    
}

snake_game.css

#root {
    -fx-background-color: black;
}

#scoreBar {
    -fx-alignment: center;
    -fx-padding: 10px;
    -fx-border-style: solid;
    -fx-border-width: 0px 0px 2px 0px;
    -fx-border-color: white;
}

#scoreIndicator {
    -fx-text-fill: white;
    -fx-font-size: 15px;
    -fx-font-family: monospace;
}

#replay {
    -fx-background-color: transparent;
    -fx-text-fill: white;
    -fx-font-size: 15px;
    -fx-font-family: monospace;
}
\$\endgroup\$
0

1 Answer 1

1
\$\begingroup\$

On originally writing this answer I had ignored your CSS because you hadn't yet included it.

You should be using your own package declarations.

Many more of your class members should be final.

I think GAME_BOARD should just be gameBoard; in fact the only variable in the entire application that currently merits all-caps is TILE_SIZE.

TILE_SIZE should be made a static.

This is personal preference, but I prefer one-type many-declarations instead of many-type many-declarations, as in

private int keyX, keyY;

But those aren't really keys, right? They're direction deltas, so should be renamed. They should also not have duplicated state between your game and board classes. Given that your board concerns itself with game coordinates, I think these are best left to the board class and removed from the game class. Rather than the game class offering getKey methods, it would call setDirection on the board as appropriate.

The game class has no constructor, which makes it impossible to make many things final. Much of your initialisation currently in start can be moved to a default constructor.

Don't pass Score: 0 to your label upon creation; do that in one place only, called for both initialisation and updates.

Your setId calls aren't strictly necessary.

Passing TILE_SIZE to your GameBoard class smells of a problem. You are conflating display/screen coordinates with game/scene coordinates. You should have your inner game logic assume that one distance unit equals one tile, and then change your projection by making your own Camera and applying it to a board-only subscene. Among other things, this will simplify your takeInput by making all delta values either -1, 0, or 1.

System.exit is the nuclear option, and you should probably prefer the JavaFX-specific Platform.exit instead.

startGame and restartGame should really just be the same thing.

GameBoard should be disposable; i.e. on every game reset you should be able to throw this instance away. To be able to do this, the pane object will need to persist, initialised and passed in from the game class. One way to represent removal of child widgets from this pane on reset is to implement AutoCloseable.

setGameBoard and resetGameBoard should just be folded into the constructor.

createEmptyTilesList is more complicated than it needs to be - you can just have a nested iteration over y and x; no need for modular math. Also, it would benefit you to represent this collection as a set instead of a list, especially if you leverage it for collision detection. Doing so would avoid the gymnastics of boundary comparison and body part iteration, and would use one set membership check.

The management and updating of this empty-tiles set should be pulled away from the snake and fruit classes and owned by GameBoard.

You should not use a lambda in runLater. Move that to a separate member function.

Often you'll want to follow the has-a pattern instead of is-a pattern, i.e. the snake class "has a" collection of body parts, and not "is a" collection of body parts. This helps with coupling.

This algorithm:

        for (int i = 1; i < size(); i++) {
            Rectangle body = get(i);
            
            double currentX = body.getX();
            double currentY = body.getY();
            
            body.setX(oldX);
            body.setY(oldY);
            
            oldX = currentX;
            oldY = currentY;
        }

is not efficient - you're moving every single body segment. Instead, you should be able to re-interpret the snake's body part collection as a queue, and only change the beginning and end. I leave this as an exercise to you.

Suggested

SnakeGame.java

package com.stackexchange.snake;

import java.util.Timer;
import java.util.TimerTask;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.scene.Camera;
import javafx.scene.ParallelCamera;
import javafx.scene.Scene;
import javafx.scene.SubScene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.input.KeyEvent;
import javafx.scene.layout.*;
import javafx.stage.Stage;


public class SnakeGame extends Application {
    private static final int
        TILE_SIZE = 20,
        ROWS = 25,
        COLUMNS = 30;

    private final BorderPane root = new BorderPane();
    private final Pane boardPane = new Pane();
    private final Label scoreIndicator = new Label();
    private final Button replay = new Button("Replay");

    // (Re)assigned on reset
    private Timer timer;
    private GameBoard gameBoard;
    private int score;

    public SnakeGame() {
        replay.setOnAction(e -> restartGame());

        Region filler = new Region();
        HBox.setHgrow(filler, Priority.ALWAYS);

        HBox scoreBar = new HBox();
        scoreBar.getChildren().addAll(scoreIndicator, filler, replay);
        root.setTop(scoreBar);

        boardPane.setOnKeyPressed(this::takeInput);

        // Values lower than 1 magnify the scene.
        // E.g. with a TILE_SIZE of 20 and COLUMNS of 30,
        //    width = 600 (screen coordinates)
        //    columns = 30 (scene coordinates)
        //    scale = 30/(20*30) = 1/20
        Camera camera = new ParallelCamera();
        double scale = 1./TILE_SIZE;
        camera.setScaleX(scale);
        camera.setScaleY(scale);

        SubScene boardScene = new SubScene(
            boardPane, TILE_SIZE * COLUMNS, TILE_SIZE * ROWS
        );
        boardScene.setCamera(camera);
        root.setCenter(boardScene);
    }

    @Override
    public void start(Stage stage) {
        Scene scene = new Scene(root);
        restartGame();
        stage.setScene(scene);
        stage.setTitle("Snake Game");
        stage.setResizable(false);
        stage.setOnCloseRequest(e -> Platform.exit());
        stage.show();
    }

    public void restartGame() {
        resetScore();

        if (gameBoard != null)
            gameBoard.close();
        gameBoard = new GameBoard(boardPane, this, ROWS, COLUMNS);
        boardPane.requestFocus();
        replay.setVisible(false);
        TimerTask task = gameBoard.new SnakeMovement();
        timer = new Timer();
        timer.scheduleAtFixedRate(task, 100, 100);
    }

    public void endGame() {
        timer.cancel();
        replay.setVisible(true);
    }

    public void resetScore() {
        score = 0;
        updateScore();
    }

    public void increaseScore() {
        score++;
        updateScore();
    }

    private void updateScore() {
        scoreIndicator.setText("Score: " + score);
    }

    private void takeInput(KeyEvent e) {
        switch (e.getCode()) {
            case DOWN -> {
                gameBoard.setDirection(0, 1);
            }
            case UP -> {
                gameBoard.setDirection(0, -1);
            }
            case RIGHT -> {
                gameBoard.setDirection(1, 0);
            }
            case LEFT -> {
                gameBoard.setDirection(-1, 0);
            }
        }
    }

    public static void main(String[] args) {
        launch(args);
    }
}

GameBoard.java

package com.stackexchange.snake;

import java.util.*;

import javafx.application.Platform;
import javafx.geometry.Point2D;
import javafx.scene.layout.Pane;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;


public class GameBoard implements AutoCloseable {
    private final Pane pane;
    private final SnakeGame game;
    private final Snake snake;
    private final Fruit fruit;
    private final Set<Point2D> emptyTiles;
    private final int tilesInRow, tilesInCol;

    private int
        dX = 0, dY = 1,
        dXQueued = dX, dYQueued = dY;

    public GameBoard(Pane pane, SnakeGame game, int tilesInRow, int tilesInColumn) {
        this.pane = pane;
        this.game = game;
        this.tilesInRow = tilesInRow;
        this.tilesInCol = tilesInColumn;
        pane.setPrefSize(tilesInRow, tilesInColumn);

        emptyTiles = getBoardTiles();

        snake = new Snake(3, tilesInColumn/2, Color.GREENYELLOW);
        pane.getChildren().addAll(snake.getRectangles());
        emptyTiles.removeAll(snake.getPoints());

        fruit = new Fruit(Color.RED);
        fruit.moveToRandom(emptyTiles);
        pane.getChildren().add(fruit);
        emptyTiles.remove(fruit.getPoint());
    }

    @Override
    public void close() {
        pane.getChildren().removeAll(snake.getRectangles());
        pane.getChildren().remove(fruit);
    }

    public Set<Point2D> getBoardTiles() {
        Set<Point2D> tiles = new HashSet<>();

        for (int y = 0; y < tilesInRow; y++) {
            for (int x = 0; x < tilesInCol; x++) {
                tiles.add(new Point2D(x, y));
            }
        }

        return tiles;
    }

    public void setDirection(int dXNew, int dYNew) {
        // the snake shouldn't move in the opposite direction
        if (dXNew != -dX && dYNew != -dY) {
            dXQueued = dXNew;
            dYQueued = dYNew;
        }
    }

    public boolean ateFruit() {
        Rectangle head = snake.getHead();
        return head.getX() == fruit.getX() &&
               head.getY() == fruit.getY();
    }

    public boolean willHit() {
        Rectangle head = snake.getHead();
        Point2D newLoc = new Point2D(
            head.getX() + dX,
            head.getY() + dY
        );
        return !(
            newLoc.equals(fruit.getPoint())
            || emptyTiles.contains(newLoc)
        );
    }

    class SnakeMovement extends TimerTask {
        private void delayed() {
            dX = dXQueued;
            dY = dYQueued;

            if (ateFruit()) {
                fruit.moveToRandom(emptyTiles);
                emptyTiles.remove(fruit.getPoint());
                snake.addHead();
                pane.getChildren().add(snake.getHead());
                game.increaseScore();
            }
            if (willHit())
                game.endGame();
            else snake.moveSnake(dX, dY, emptyTiles);
        }

        @Override
        public void run() {
            Platform.runLater(this::delayed);
        }
    }
}

Fruit.java

package com.stackexchange.snake;

import java.util.Random;
import java.util.Set;

import javafx.geometry.Point2D;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;

public class Fruit extends Rectangle {
    private static final Random random = new Random();

    public Fruit(Color color) {
        super(1, 1, color);
    }

    public void moveToRandom(Set<Point2D> emptyTiles) {
        int i = 0, randomIndex = random.nextInt(emptyTiles.size());

        for (Point2D tile: emptyTiles) {
            if (i == randomIndex) {
                setX(tile.getX());
                setY(tile.getY());
                break;
            }
            i++;
        }
    }

    public Point2D getPoint() {
        return new Point2D(getX(), getY());
    }
}

Snake.java

package com.stackexchange.snake;

import java.util.*;
import java.util.stream.Collectors;

import javafx.geometry.Point2D;
import javafx.scene.paint.Color;
import javafx.scene.shape.Rectangle;


public class Snake {
    private final Color color;
    private final List<Rectangle> rectangles = new ArrayList<>();

    public Snake(int initialLength, int startX, Color color) {
        this.color = color;

        for (int startY = initialLength - 1; startY >= 0; startY--) {
            // 0 is the head and 2 is the tail, if snake's initial length is 3,
            // startY will be 40, 20, and 0, for elements 0, 1, and 2 respectively
            // since the snake is moving downward
            rectangles.add(createBodyPart(startX, startY));
        }
    }

    public List<Rectangle> getRectangles() {
        return Collections.unmodifiableList(rectangles);
    }

    public Set<Point2D> getPoints() {
        return rectangles.stream()
            .map(rect -> new Point2D(rect.getX(), rect.getY()))
            .collect(Collectors.toSet());
    }

    private Rectangle createBodyPart(double x, double y) {
        Rectangle body = new Rectangle(x, y, 1, 1);
        body.setFill(color);
        return body;
    }

    public void moveSnake(int dX, int dY, Set<Point2D> emptyTiles) {
        Rectangle head = getHead(),
                  second = rectangles.get(1);
        boolean isHeadAdded =
            head.getX() == second.getX() &&
            head.getY() == second.getY();
        double oldX = head.getX(), newX = oldX + dX,
               oldY = head.getY(), newY = oldY + dY;
        head.setX(newX);
        head.setY(newY);

        // the head now occupies this tile
        emptyTiles.remove(new Point2D(newX, newY));

        // when a new head is added, the rest of the body stays still for one task
        // so there is no need to go through the loop, and the tail's place shouldn't
        // be added to emptyTiles
        if (isHeadAdded)
            return;

        for (int i = 1; i < rectangles.size(); i++) {
            Rectangle body = rectangles.get(i);

            double currentX = body.getX(),
                   currentY = body.getY();
            body.setX(oldX);
            body.setY(oldY);
            oldX = currentX;
            oldY = currentY;
        }

        // the old tile of the tail is now empty
        emptyTiles.add(new Point2D(oldX, oldY));
    }

    public void addHead() {
        // the new head is set on the same tile as the old head initially
        // but it will be moved in the moveSnake method afterwards
        Rectangle oldHead = getHead(),
                  newHead = createBodyPart(oldHead.getX(), oldHead.getY());
        rectangles.add(0, newHead);
    }

    public Rectangle getHead() {
        return rectangles.get(0);
    }
}

Output

GUI

\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.