13
\$\begingroup\$

I am a self-taught coder, and have been learning Java for the last 2 months.

I have recently created a snake game in Java with the help of an online tutorial. I have rearranged the program into separate classes & methods. I am skeptical on how good of a programmer I am, and I code messy.

I don't really want help on how to make the game better in visuals, levels, or anything like that. I am asking those who are more advanced programmers to help me with implementing programming techniques, better organization, how to comment better, etc. Pretty much anything that will help me become a better programmer.

Also, I have commented a few things out of pure guess, so if it appears as if I have gotten something wrong, please correct me!

Also, any extra tips for my future projects would be much appreciated!

Node

public class Node {

    private final int x;
    private final int y;

    public Node(int x, int y) {
        this.x = x;
        this.y = y;
    }

    public int getX() {
        return x;
    }

    public int getY() {
        return y;
    }
}

Snake

import java.util.LinkedList;

public class Snake {

    private LinkedList<Node> body = new LinkedList<Node>();

    public boolean isEatFood(Node food) {
        Node head = body.getFirst();
        return Math.abs(head.getX() - food.getX()) + Math.abs(head.getY() - food.getY()) == 0;
    }

    public Node move(Direction direction) {
        Node node = null;
        int headX = this.body.getFirst().getX();
        int headY = this.body.getFirst().getY();
        switch(direction) {
            case UP :
                node = new Node(headX, headY - 1);
                break;
            case RIGHT :
                node = new Node(headX + 1, headY);
                break;
            case DOWN :
                node = new Node(headX, headY + 1);
                break;
            case LEFT :
                node = new Node(headX - 1, headY);
                break;
        }
        this.body.addFirst(node);
        return body.removeLast();
    }

    public Node getHead() {
        return body.getFirst();
    }

    public Node addTail(Node area) {
        this.body.addLast(area);
        return area;
    }

    public LinkedList<Node> getBody() {
        return body;
    }
}

Grid

import java.util.Arrays;
import java.util.Random;

public class Grid {

    private boolean isCovered[][];
    private final int width;
    private final int height;
    private int scores = 0;

    private Snake snake;
    private Node food;

    private Direction snakeDirection = Direction.LEFT;         // initial direction is LEFT 
    public boolean isDirectionChanged = false;

    public Grid(int width, int height) {
        this.width = width;
        this.height = height;
        isCovered = new boolean[width][height];

        initSnake();
        createFood();
    }
    private Snake initSnake() {                                 // initial Snake has 3 Node
        snake = new Snake();
        for (int i = 0; i < 3; i++) {
            snake.addTail(new Node(i + width / 2, height / 2));
            isCovered[i + width / 2][height / 2] = true;
        }
        return snake;
    }

    public Node createFood() {
        int x,y;
        do {
            x = new Random( ).nextInt(width);
            y = new Random( ).nextInt(height);
        } while (isCovered[x][y] == true);
        food = new Node(x, y);
        return food;
    }

    public boolean nextRound() {                                     //follow the direction and move one step
        if (isMoveValid(snakeDirection)) {
            Node move = snake.move(snakeDirection);
            if (snake.isEatFood(food)) {                             //if ate food, add the Node moved at tail
                snake.addTail(move);
                createFood();
                System.out.println(++scores);
            } else isCovered[move.getX()][move.getY()] = false;
            return true;
        } else return false;
    }
    private boolean isMoveValid(Direction direction) {
        int headX = snake.getHead().getX();
        int headY = snake.getHead().getY();
        switch(direction) {
            case UP :
                headY--;
                break;
            case RIGHT :
                headX++;
                break;
            case DOWN :
                headY++;
                break;
            case LEFT :
                headX--;
                break;
        }
        if (headX < 0 || headX >= width || headY < 0 || headY >= height) return false;
        if (isCovered[headX][headY] == true) return false;
        isCovered[headX][headY] = true;
        return true;
    }

    public void changeDirection(Direction newDirection) {
        if (snakeDirection.compatibleWith(newDirection)) {
            snakeDirection = newDirection;
            isDirectionChanged = true;
        }
    }

    public Snake getSnake() { return snake; }
    public Node getFood() { return food; }
    public int getWidth() { return width; }
    public int getHeight() { return height; }
    public int getScore() { return scores; }

}

GameController

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class GameController implements Runnable, KeyListener{

    private final Grid grid;
    private final GameView gameView;

    private boolean running;

    public GameController(Grid grid, GameView gameView) {
        this.grid = grid;
        this.gameView = gameView;
        this.running = true;
    }

    @Override
    public void run() {
        while (running) {
            try {
                Thread.sleep(Math.max(50, 200 - grid.getScore() / 5 * 30));                     //DEFAULT_MOVE_INTERVAL
            } catch (InterruptedException e) {
                break;
            }
            grid.isDirectionChanged = false;
            if (grid.nextRound() == true) {
                gameView.draw();
            } else {
                System.out.print("Congraduations! Your scores: " + grid.getScore());
                gameView.showGameOverMessage();
                running = false;
            }
        }
    }

    @Override
    public void keyPressed(KeyEvent e) {
        int keyCode = e.getKeyCode();
        if (grid.isDirectionChanged == false) {
            switch (keyCode) {
                case KeyEvent.VK_UP :
                    grid.changeDirection(Direction.UP);
                    break;
                case KeyEvent.VK_RIGHT :
                    grid.changeDirection(Direction.RIGHT);
                    break;
                case KeyEvent.VK_DOWN :
                    grid.changeDirection(Direction.DOWN);
                    break;
                case KeyEvent.VK_LEFT :
                    grid.changeDirection(Direction.LEFT);
                    break;
                case KeyEvent.VK_SPACE :
                    break;
            }
        }
        // repaint the canvas
    }
    @Override
    public void keyReleased(KeyEvent e) {}
    @Override
    public void keyTyped(KeyEvent e) {}

}

SnakeApp

import javax.swing.*;
import java.awt.*;

public class SnakeApp implements Runnable {

    private final int DEFAULT_GRID_WIDTH = 30;
    private final int DEFAULT_GRID_HEIGHT = 30;
    private GameView gameView;
    private GameController gameController;

    public void run() {

        JFrame window = new JFrame("Snake Game");               //creat game window

        Container contentPane = window.getContentPane();
        // use Grid initialize the gamaView
        Grid grid = new Grid(DEFAULT_GRID_WIDTH, DEFAULT_GRID_HEIGHT);
        gameView = new GameView(grid);
        gameView.init();

        // set JPanel's size
        gameView.getCanvas().setPreferredSize(new Dimension(DEFAULT_GRID_WIDTH * gameView.DEFAULT_NODE_SIZE, 
            DEFAULT_GRID_HEIGHT * gameView.DEFAULT_NODE_SIZE));
        // add JPanel to windows
        contentPane.add(gameView.getCanvas(), BorderLayout.CENTER);

        // draw grid and snake
        window.pack();
        window.setResizable(false);
        window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        window.setVisible(true);

        gameController = new GameController(grid, gameView);
        window.addKeyListener(gameController);

        // start the thread
        new Thread(gameController).start();

    }

    public static void main(String[] args) {
        //SnakeApp snakeApp = new SnakeApp();
        //snakeApp.run();
        SwingUtilities.invokeLater(new SnakeApp());
    }
}

GameView

import javax.swing.*;
import java.awt.*;

public class GameView {
    //use Graphics API draw pics
    private JPanel canvas;
    private final Grid grid;
    public final int DEFAULT_NODE_SIZE = 15;

    public GameView(Grid grid) {
        this.grid = grid;
    }

    public void init() {
        canvas = new JPanel() {
            @Override
            public void paintComponent(Graphics graphics) {
                drawGridBackground(graphics);
                drawSnake(graphics, grid.getSnake());
                drawFood(graphics, grid.getFood());
            }
        };
    }

    public void draw() {
        canvas.repaint();
    }

    public JPanel getCanvas() {
        return canvas;
    }

    public void drawSnake(Graphics graphics, Snake snake) {
        for (Node node : snake.getBody()) {
            drawSquare(graphics, node, Color.GREEN);
        }
    }
    public void drawFood(Graphics graphics, Node squareArea) {
        drawCircle(graphics, squareArea, Color.RED);
    }
    public void drawGridBackground(Graphics graphics) {
        for (int i = 0; i < grid.getWidth(); i++) {
            for (int j = 0; j < grid.getHeight(); j++) {
                drawSquare(graphics, new Node(i, j), new Color(127, 127, 127, 255));
            }
        }
    }

    private void drawSquare(Graphics graphics, Node squareArea, Color color) {
        graphics.setColor(color);
        int size = DEFAULT_NODE_SIZE;                                 
        graphics.fillRect(squareArea.getX() * size, squareArea.getY() * size, size - 1, size - 1);
    }
    private void drawCircle(Graphics graphics, Node squareArea, Color color) {
        graphics.setColor(color);
        int size = DEFAULT_NODE_SIZE;
        graphics.fillOval(squareArea.getX() * size-1, squareArea.getY() * size-1, size, size);
    }

    public void showGameOverMessage() {
        JOptionPane.showMessageDialog(null, "Game Over!", "GameOver", JOptionPane.INFORMATION_MESSAGE);
    }

}
\$\endgroup\$
  • \$\begingroup\$ Did you forget to add class Direction? \$\endgroup\$ – Timothy Truckle Feb 26 '17 at 13:32
1
\$\begingroup\$

First of all, this is a great achievement for a self-taught programmer of only a few months. Pat yourself on the back! Good formatting, good separation of concerns, mostly good variable names, methods are short and do one thing.

You are missing the Direction.java file. I created one to see if the program would run. It does and it's fun!

public enum Direction {
    LEFT, RIGHT, UP, DOWN;

    public boolean compatibleWith(Direction newDirection) {
        if (this.equals(LEFT) || this.equals(RIGHT)) {
            return UP.equals(newDirection) || DOWN.equals(newDirection); 
        } else {
            return LEFT.equals(newDirection) || RIGHT.equals(newDirection);
        }
    }
}

Ways to Improve

  • Not enough comments. All public methods should be commented unless they are truly trivial, like getters and setters. Don't put comment in the right margin.
  • With a project this size, you should have a package name. Having all the files in one package would be fine, but better would be to have model, controller view. Your classes are already set up this way.
  • I would rename Grid.java to GameGrid or SnakeGrid. "Grid" is a little too generic for my taste. (Nitpick).

SnakeApp.java

These lines:

private final int DEFAULT_GRID_WIDTH = 30;
private final int DEFAULT_GRID_HEIGHT = 30;

...should be static too if they're going to be constants are the variable names imply.

Node.java

I would make this class final. Good job making it immutable.

Grid.java

This is one of only a few places when your formatting is poor:

public boolean nextRound() {                                     //follow the direction and move one step
    if (isMoveValid(snakeDirection)) {
        Node move = snake.move(snakeDirection);
        if (snake.isEatFood(food)) {                             //if ate food, add the Node moved at tail
            snake.addTail(move);
            createFood();
            System.out.println(++scores);
        } else isCovered[move.getX()][move.getY()] = false;
        return true;
    } else return false;
}

Never put the else clause on the same on the same line as else. Put it in braces. If fact, I put braces around all the clauses in an if statement. It's clearer and less likely to cause bugs when being maintained.

Snake.java

I this statement:

private LinkedList<Node> body = new LinkedList<Node>();

...you don't need to repeat the generic type on the right-hand side. This will do:

private LinkedList<Node> body = new LinkedList<>();

There's no reason to use this with fields unless you're disambiguating it from a parameter.

In the method move(), your switch statement needs a default case where you set node to null. Then you don't have to initialize node to null in the declaration.

GameController

Don't write:

if (grid.nextRound() == true) {

...just write

if (grid.nextRound()) {

Now you can see the "nextRound" may not be the best name for that method.

I think the boolean running can be a local variable instead of a field.

\$\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.