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);
 }
}
200_success
145k22 gold badges190 silver badges478 bronze badges
asked Feb 24, 2017 at 8:28
\$\endgroup\$
1
  • \$\begingroup\$ Did you forget to add class Direction? \$\endgroup\$ Commented Feb 26, 2017 at 13:32

1 Answer 1

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.

answered Mar 3, 2017 at 14:53
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.