Program Your Own 2048 Game W/Java!

by PranP1 in Circuits > Software

20935 Views, 5 Favorites, 0 Comments

Program Your Own 2048 Game W/Java!

4oa6ba.gif
Program Your Own 2048 Game W:Java!.png

I love the game 2048. And so I decided to program my own version.

It's very similar to the actual game, but programming it myself gives me the freedom to change whatever I want whenever I want to. If I want a 5x5 game instead of the typical 4x4, a simple change using the 'Board' constructer will allow me to do so. Say I want to make the game more difficult, adding pieces at positions that will make it most complex for the player rather than at random. Using a simple algorithm, I can do just that. While I won't cover all these modifications in this Instructable, I do plan on adding more as I go.

For now, however, we'll program your typical game of 2048.

Let's get started!

(A side note: This Instructable requires moderate knowledge of programming - specifically with Java)

Materials

127235502_762176114376934_8920269776512909402_n.jpg

You won't need much for this project as it is just a programming walkthrough.

Materials:

  • Laptop
  • Eclipse (or any IDE of your choice)

Yup. That's it.

Get to Know the Program - Board

I uploaded all my code onto GitHub - check it out here: https://github.com/patturtestsite/2048

I divided the game up into 3 classes: Board, Tile and Game.

Board:

Description: The Board class deals with the gameboard, setting up an array of 'Tile' elements, getting the current score and the highest tile, and putting the array in a string (to be used later in 'Game'). Most of the logic is also here, the class providing methods for spawning 2's and 4's at random locations, moving up, down, left and right, and letting players know when the game is over.

Constructors:

/* Default constructor for the Board - sets up a 4x4 matrix */

public Board() {...}

/* Constructor for the Board - sets up a matrix with specified grid size */

public Board (int grids) {...}

Methods:

/* Getter method that returns the board */

public Tile[][] getBoard() {...}

/* Getter method that returns the score */

public int getScore() {...}

/* Finds the highest tile on the board and returns it */

public int getHighTile() {...}

/* Prints out the board onto the console - for testing purposes */

public void print() {...}

/* Returns the board as a String - used in the GUI */

public String toString() {...}

/* Spawns a 2 (or 4) at an empty space very time a move is made */

public void spawn() {...}

/* Checks to see if the board is completely blacked out and if it is, it will nudge the players to restart */

public boolean blackOut() {...}

/* Checks to see if the game is over - when the board is blacked out and none of the tiles can combine */

public boolean gameOver() {...}

/* Called when 'w' or up arrow is pressed - calls 'verticalMove' for every tile on the board with parameter 'up' */

public void up() {...}

/* Called when 's' or down arrow is pressed - calls 'verticalMove' for every tile on the board with parameter 'down' */
public void down() {...}

/* Called when 'd' or right arrow is pressed - calls 'horizontalMove' for every tile on the board with parameter 'right' */
public void right() {...}

/* Called when 'a' or left arrow is pressed - calls 'horizontalMove' for every tile on the board with parameter 'left' */

public void left() {...}

/* Compares two tile's values together and if they are the same or if one is equal to 0 (plain tile) - their values are added (provided that the tiles we are comparing are two different tiles and they are moving towards the appropriate direction) - recursively moves through the row */

public void horizontalMove (int row, int col, String direction) {...}

/* Compares two tile's values together and if they are the same or if one is equal to 0 (plain tile) - their values are added (provided that the tiles we are comparing are two different tiles and they are moving towards the appropriate direction) - recursively moves through the column */

public void verticalMove (int row, int col, String direction) {...}

Yeah, that's a lot of methods - but don't worry, most are extremely easy to understand. On top of that, the 'Board' class is the most complex, so everything after this will be relatively simple.

Get to Know the Program - Tile

Tile:

Description: The Tile class deals with the individual tiles, and is the smallest of all the classes. Each tile has an integer value and a color. It has two constructors that create Tiles of value 0 (default) or value #. The methods are mostly self explanatory, with 'getter' and 'setter' methods making up a bulk of the total.

Constructors:

/* Constructs a basic tile with a value of 0 */

public Tile() {...}

/* Constructs a tile with a value of number */

public Tile (int number) {...}

Methods:

/* Gets the tile's value */

public int getValue() {...}

/* Sets the tile's value - used when adding two tiles together */

public void setValue (int value) {...}

/* Represents the tile as a String - used in the GUI */

public String toString() {...}

/* Sets the tile's color based on its value */

public void setColor() {...}

/* Gets the tile's color */

public void getColor() {...}

Get to Know the Program - Game

Game

Description: The Game Class houses the main method, most of the GUI methods and the Key interactions. It takes both the Tile and Board classes, and enables them to work together.

Constructors:

None

Methods:

/* sets up the GUI with appropriate sizes and adds a Key Listener */

public static void setUpGUI() {...}

/* Checks to see whether wasd or arrow keys are pressed and performs the appropriate actions - updates the JFrame with every move */

public void keyPressed (KeyEvent e) {...}

/* Paints the GUI with a series of strings, the board, the tiles and ensures they are repainted when the game is over */

public void paint (Graphics g) {...}

/* draws an individual tile - called from the paint method */

public void drawTiles (Graphics g, Tile tile, int x, int y) {...}

/* Main method - sets up the GUI and starts the game */

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

Important Methods - Movement

The movement methods are the most important to understand, but the good news is once you understand the vertical movements, you can apply that understanding to the horizontal movements. In fact, the three vertical movement methods are the exact same as the three horizontal method movements, except one moves across rows and the other across columns. For that reason, let's focus on just the vertical movement methods.

private void verticalMove( int row, int col, String direction )
    {
        Tile initial = board[border][col];
        Tile compare = board[row][col];
        if ( initial.getValue() == 0 || initial.getValue() == compare.getValue() )
        {
            if ( row > border || ( direction.equals( "down" ) && ( row < border ) ) )
            {
                int addScore = initial.getValue() + compare.getValue();
                if ( initial.getValue() != 0 )
                {
                    score += addScore;
                }
                initial.setValue( addScore );
                compare.setValue( 0 );
            }
        }
        else
        {
            if ( direction.equals( "down" ) )
            {
                border--;
            }
            else
            {
                border++;
            }
            verticalMove( row, col, direction );
        }
    }

The above method, verticalMove, is called by the 'up' and 'down' methods. Let's take a look at the 'up' method.

public void up()
    {
        for ( int i = 0; i < grids; i++ )
        {
            border = 0;
            for ( int j = 0; j < grids; j++ )
            {
                if ( board[j][i].getValue() != 0 )
                {
                    if ( border <= j )
                    {
                        verticalMove( j, i, "up" );
                    }
                }
            }
        }
    }

This method goes through the entire board and calls verticalMove for every tile with the parameter "up". verticalMove then compares the tile at position 'j' and 'i' with the tile at position 'border' and 'i'. If the two are equal, they are combined. If they are not, the border tile is increased by 1 (as the parameter in place is 'up'), and verticalMove is called again.

Check out the comments on my GitHub for additional information.

If we want to apply this to a horizontal movement, we simply need to change our perspective from columns to rows.

Important Methods - Game Over

The Game Over method uses several if statements to check whether or not the game is over. Because 2048 has several circumstances that make it seem as though the game is over, but it may really not be over, I had to make sure everything was covered, and if/else statements seemed to be the easiest way to do that. As a result, the method is extremely long, but relatively basic to understand.

public boolean gameOver()
    {
        int count = 0;
        for ( int i = 0; i < board.length; i++ )
        {
            for ( int j = 0; j < board[i].length; j++ )
            {
                if ( board[i][j].getValue() > 0 )
                {
                    if ( i == 0 && j == 0 )
                    {
                        if ( board[i][j].getValue() != board[i + 1][j].getValue()
                            && board[i][j].getValue() != board[i][j + 1].getValue() )
                        {
                            count++;
                        }
                    }
                    else if ( i == 0 && j == 3 )
                    {
                        if ( board[i][j].getValue() != board[i + 1][j].getValue()
                            && board[i][j].getValue() != board[i][j - 1].getValue() )
                        {
                            count++;
                        }
                    }
                    else if ( i == 3 && j == 3 )
                    {
                        if ( board[i][j].getValue() != board[i - 1][j].getValue()
                            && board[i][j].getValue() != board[i][j - 1].getValue() )
                        {
                            count++;
                        }
                    }
                    else if ( i == 3 && j == 0 )
                    {
                        if ( board[i][j].getValue() != board[i - 1][j].getValue()
                            && board[i][j].getValue() != board[i][j + 1].getValue() )
                        {
                            count++;
                        }
                    }
                    else if ( i == 0 && ( j == 1 || j == 2 ) )
                    {
                        if ( board[i][j].getValue() != board[i + 1][j].getValue()
                            && board[i][j].getValue() != board[i][j + 1].getValue()
                            && board[i][j].getValue() != board[i][j - 1].getValue() )
                        {
                            count++;
                        }
                    }
                    else if ( i == 3 && ( j == 1 || j == 2 ) )
                    {
                        if ( board[i][j].getValue() != board[i - 1][j].getValue()
                            && board[i][j].getValue() != board[i][j + 1].getValue()
                            && board[i][j].getValue() != board[i][j - 1].getValue() )
                        {
                            count++;
                        }
                    }
                    else if ( j == 0 && ( i == 1 || i == 2 ) )
                    {
                        if ( board[i][j].getValue() != board[i][j + 1].getValue()
                            && board[i][j].getValue() != board[i - 1][j].getValue()
                            && board[i][j].getValue() != board[i + 1][j].getValue() )
                        {
                            count++;
                        }
                    }
                    else if ( j == 3 && ( i == 1 || i == 2 ) )
                    {
                        if ( board[i][j].getValue() != board[i][j - 1].getValue()
                            && board[i][j].getValue() != board[i - 1][j].getValue()
                            && board[i][j].getValue() != board[i + 1][j].getValue() )
                        {
                            count++;
                        }
                    }
                    else
                    {
                        if ( board[i][j].getValue() != board[i][j - 1].getValue()
                            && board[i][j].getValue() != board[i][j + 1].getValue()
                            && board[i][j].getValue() != board[i - 1][j].getValue()
                            && board[i][j].getValue() != board[i + 1][j].getValue() )
                        {
                            count++;
                        }
                    }
                }
            }
        }
        if ( count == 16 )
        {
            return true;
        }
        return false;
    }

Essentially the method begins with a count that is equal to 0, and for every immovable/not-combine-able tile, one is added. If the count equals 16 by the end (the total number of tiles on the board), then that means the game is over, as not a single tile is able to move.

Important Methods - Key Presses

The Key Press methods are imported over from keyListener. I did not use keyReleased or keyTyped, as I found keyPressed to be the most convenient for my purposes. I also had to import KeyEvent in order to use it as a parameter. While most of this can be found online and is pretty self-explanatory for someone with programming knowledge, I thought I'd go ahead and explain it a little more here.

public void keyPressed( KeyEvent e )
    {
        if ( e.getKeyChar() == 'w' || e.getKeyCode() == KeyEvent.VK_UP )
        {
            game.up();
            game.spawn();
            gameBoard = game.toString();
            frame.repaint();
        }
        else if ( e.getKeyChar() == 's' || e.getKeyCode() == KeyEvent.VK_DOWN )
        {
            game.down();
            game.spawn();
            gameBoard = game.toString();
            frame.repaint();
        }
        else if ( e.getKeyChar() == 'a' || e.getKeyCode() == KeyEvent.VK_LEFT )
        {
            game.left();
            game.spawn();
            gameBoard = game.toString();
            frame.repaint();
        }
        else if ( e.getKeyChar() == 'd' || e.getKeyCode() == KeyEvent.VK_RIGHT )
        {
            game.right();
            game.spawn();
            gameBoard = game.toString();
            frame.repaint();
        }
        else if ( e.getKeyCode() == KeyEvent.VK_ENTER )
        {
            game = new Board();
            game.spawn();
            game.spawn();
            frame.repaint();
        }
    }

Essentially, this method checks whether the keyEvent is a w, a, s, or d - or an up, down, left or right key. As with most video games, 'w' and up serve the same purpose, 's' and down serve the same purpose, 'a' and left serve the same purpose, and 'd' and right serve the same purpose. The Enter key is intended to start the game and as such, everytime it is pressed, a new Board is painted and two numbers are randomly spawned. If any of the other keys are pressed, the game is moved up, down, left or right based on which one is pressed and the frame is repainted.

You're Done!

Hopefully with a better understanding of the classes, their constructors, and their methods, you are able to both recreate the game and play around with the code, modifying it to your liking. If you have any questions, definitely let me know in those comments below.