CSE-8B / PA4 / writeup / writeup.md
writeup.md
Raw

PA 4: 2048

Assignment Overview

Due date: Thursday, February 06 @ 11:59PM Note that this due date is not Tuesday as the calendar initially suggested. We are extending it to give some slack since there is some overlap with PA3, but don't wait to get started, start early and start often!

Getting Started

There are a number of ways to get started on development. The following is the recommended way to ensure that your code will compile during grading.

  1. If you are using your own machine or are on a lab computer to complete the assignment, go to step 2 directly. Otherwise, ssh into your cs8bwi20 account.
    • ssh cs8bwi20__@ieng6.ucsd.edu
  2. Acquire the starter files.
    • From ieng6:
      • Log in to your cs8bwi20 account.
      • From the command line, use the command cp -r ~/../public/pa4 ~/ (this will copy the entire starter files directory to your home directory)
      • Type ls ~ to verify that you have copied the pa4 directory over.
    • From GitHub:
      • git clone https://github.com/CaoAssignments/cse8b-wi20-pa4-2048-starter.git
      • Alternatively, you can download the repo as a zipped folder.
  3. If you downloaded the repo as a zipped folder, navigate to it through your terminal or text editor (Atom, Eclipse, etc.). If you git cloned the repo, you can switch into that directory immediately.
    • cd cse8b-wi20-pa4-2048-starter
    • Optional: You may choose to rename this repo. You can do this by using this command:
      • mv cse8b-wi20-pa4-2048-starter pa4
  4. You can now start working on it through vim using the following command or open the directory in your preferred editor.
    • vim GameState.java or gvim GameState.java
  5. To compile your code, use the javac command.
    • Syntax: javac file1.java file2.java etc...
    • Example: javac GameState.java
  6. To run your code, use the java command passing in the name of the class with the main method that you want to run.
    • Syntax: java nameOfClass
    • Example: java GameState

2048 Background

2048 is a single-player puzzle game created in March 2014 by 19-year-old Italian web developer Gabriele Cirulli, in which the objective is to slide numbered tiles on a grid to combine them and create a tile with the number 2048. Cirulli created the game in a single weekend as a test to see if he could program a game from scratch. 2048 was an instant hit when the game received over 4 million visitors in less than a week.

The original version of the game was programmed in JavaScript. You will be creating a Java version of the 2048 game that can be played in the terminal (text based). Here is a link if you don’t know of this game http://2048game.com/. Your version of the game will have the exact same functionality as the original version of the game.

In this PA you will be implementing the infrastructure of the game.

Basic functionalities

2048 is played on a simple grid of size NxN (4x4 by default), with numbered tiles (all powers of 2) that will slide in one of 4 directions (UP, DOWN, LEFT, RIGHT) based on input from the user. Every turn, a new tile will randomly appear in an empty spot on the board with a value of either 2 or 4 (determined randomly with a 90% probability of being a tile with value 2). Tiles will slide as far as possible in the chosen direction until they are stopped by either another tile or the edge of the grid. If two tiles of the same number collide while moving, they will merge into a tile with a total value of the two tiles that collide. For example, if two tiles of value 8 slide into one another the resulting tile will have a value of 16. The resulting tile cannot merge with another tile again in the same move.

A score will also be kept based on the user’s performance. The user’s score starts at zero, and is increased whenever two tiles combine, by the value of the newly combined tile. For example, if two tiles of value 8 are merged together, the resulting tile will have a value of 16 and the user’s score will be increased by 16.

Provided Files

DO NOT MODIFY THESE FILES. We will use our own copy when grading, so any modifications you make will not be carried over. The only file you should change is Config.java but do not add anything that does not already exist in the file; you should only change the value of these constants to test your code.

Config.java

This file contains constants and settings for playing the game. These are magic numbers that define the configuration of the game, so you must place your own magic numbers in their respective files.

Direction.java

This file contains an enum. You can read the file for more information.

Game2048.java

This file implements functionality for playing the game. You don't need to know how this file works to write your code in GameState.java.

PlayManager.java

This file starts and plays the game.

Your Task: GameState.java

You will be writing a file called GameState.java. You will need to create this file from scratch. Make sure that it contains a public class called GameState with the following functionality. Since we are not giving you starter code, you should make sure your file follows the strict method signatures we give to you, otherwise the autograder will not be able to run. We have defined what each method is required to do, but you are free to implement it however. In particular, we will only be grading based on what a function call does to the returned value of getBoard() and getScore() and what the called function returns (see below), so you are free to store the backend representation of your game however you wish.

NOTE: Some of these methods are REQUIRED. If you do not implement them, you will fail their respective autograder tests. Make sure that your method signatures for these are exactly as noted on the writeup - this is the most likely reason your code "compiles on your own computer but not on Gradescope."

OPTIONAL: Instance Variables

There are three suggested instance variables (you are free to use whatever variables you think might help you, these are what we used):

private Random rng; 
private int[][] board; 
private int score; 
  • rng: we used this everytime we wanted to generate a number so that we didn't need to create a new Random object every time.
  • board: we used this to keep track of the board. For tiles that had no value (empty), we had a 0 in board.
  • score: we used this to keep track of the score

REQUIRED: public String toString ()

Add the following code to your GameState.java so your game board will be displayed in the correct format. You may have to fix the style of this code to consistent with your own styling. Do not implement your own toString() method otherwise your game may have undefined behavior.

// REQUIRED - put this in your GameState.java
public String toString () {
    StringBuilder outputString = new StringBuilder();
    outputString.append(String.format("Score: %d\n", getScore()));
    for (int row = 0; row < getBoard().length; row++) {
        for (int column = 0; column < getBoard()[0].length; column++) {
            outputString.append(getBoard()[row][column] == 0 ? "    -" :
                String.format("%5d", getBoard()[row][column]));
        }
        outputString.append("\n");
    }
    return outputString.toString();
}

You must add this method to your GameState.java and do not implement your own toString().

Constructor

public GameState (int numRows, int numCols); // REQUIRED

This is the sole constructor for the GameState class. To implement it, you will use the parameters passed in to initialize your instance variables. In this constructor, you should:

  1. Create a board with numRows rows and numCols columns. This should be an empty board. You don't need to do any checks for the validity of numRows and numCols.
  2. Set your starting score to 0.

If you choose to create your Random object here, you may want to use the variable RANDOM_SEED in Config.java as the argument (seed). The sequence of random numbers generated by the Random object is solely determined by the value of the seed, meaning that each time you run the program you will get the same sequence of numbers. If you do not specify a seed, then a seed is used depending on the literal time of the initialization of the object.

You must implement this method with the given method signature and it must have the specified behavior.

Getters and Setters

public int[][] getBoard (); // REQUIRED
public void setBoard (int[][] newBoard); // REQUIRED
public int getScore (); // REQUIRED
public void setScore (int newScore); // REQUIRED

REQUIRED: public int[][] getBoard ()

In this method, you should return a deep copy of the board of GameState class. Although you are free to represent your board however you want, we require the return board to be a 2d-array with the appropriate number of rows and columns. Each element of the 2d-array should either be the value of the tile or 0 if the tile does not exist. This method should not make any changes to the game state.

You must implement this method with the given method signature and it must have the specified behavior.

REQUIRED: public void setBoard (int[][] newBoard)

In the method, you should make a deep copy of the input board and set the board of GameState class to be the deep-copied board. For null inputs, make no change and return immediately. The format of the input 2d array is the same as the format of the output 2d array from getBoard(). This method should not make any changes to the game state other than setting the new board state.

You must implement this method with the given method signature and it must have the specified behavior.

REQUIRED: public int getScore ()

Return the current score. This method should not make any changes to the game state.

You must implement this method with the given method signature and it must have the specified behavior.

REQUIRED: public void setScore (int newScore)

Sets the current score to be the input score. Note that this should also allow us to set a negative score. This method should not make any changes to the game state other than setting the score.

You must implement this method with the given method signature and it must have the specified behavior.

Generating Tiles

protected int rollRNG (int bound); // optional
protected int randomTile (); // REQUIRED
protected int countEmptyTiles (); // REQUIRED
protected int addTile (); // REQUIRED

OPTIONAL: protected int rollRNG (int bound)

Return a random integer between 0(inclusive) and bound(exclusive). You should use the nextInt() method of the Random class.

REQUIRED: protected int randomTile ()

In the game of 2048, a tile numbered either 2 or 4 is added to the board after each successful move/slide. You should be using this method to decide which kind of tile should be added to the board whenever a new tile is needed. Return the int value 2 with probability TWO_PROB (from the Config.java file, where TWO_PROB=70 means 70% chance) and 4 with the remaining probabilty. This method should not make any changes to the game state.

You must implement this method with the given method signature and it must have the specified behavior.

REQUIRED: protected int countEmptyTiles ()

Return the number of empty tiles on the board. This method should not make any changes to the game state.

You must implement this method with the given method signature and it must have the specified behavior.

REQUIRED: protected int addTile ()

In this method, add a new tile to the board. You should return the type of the tile added, either 2 or 4 (this must be whatever randomTile() returns to us). Note that we might theoretically call this method on a full board, in which case we wouldn't be able to add any tiles. Return 0 if you are unable to add any tile. This method should not make any changes to the game state other than adding the new tile.

You must implement this method with the given method signature and it must have the specified behavior.

  1. First, you should check whether there are empty tiles or not on the board. If there is not empty tile, don't add any tiles to the board and return 0. If there are empty tiles, continue to step 2.
  2. Randomly choose a tile out of all the empty tiles (you can use rollRNG() to help you choose the spot to add the tile). Then, add the tile decided by randomTile(), and add it to the board. Return the type of the tile just added to the board (i.e. return 2 if you added a 2 and return 4 if you added a 4).

Movement

protected void rotateCounterClockwise (); // optional
protected boolean canSlideDown (); // REQUIRED
public boolean isGameOver (); // REQUIRED
protected boolean slideDown (); // REQUIRED
public boolean move (Direction dir); // REQUIRED

OPTIONAL: protected void rotateCounterClockwise ()

Rotate the board counter-clockwise once. You may want to read the specification for the move() method before deciding whether or not to implement this method. This should be very similar to how you did the transposition for PA3. You are not required to implement this method.

REQUIRED: protected boolean canSlideDown ()

Use this method to determine if you can slide down. You will return true if sliding down is possible and false otherwise. This method should not make any changes to the game state.

In particular, there are two cases where sliding down is possible:

  1. If two tiles can be combined. Two tiles can be combined if they share the same value and there are no other tiles between them (empty tiles can be between them).
  2. If there is an empty tile below a non-empty tile.

More precisely, this method should return true if slideDown() (defined below) should change the state of the board and return false if slideDown() should not be able to change the state of the board.

You must implement this method with the given method signature and it must have the specified behavior.

REQUIRED: public boolean isGameOver ()

A game is over when you cannot move in any direction. To implement isGameOver(), use rotateCounterClockwise() and canSlideDown() to see if movement in any direction is possible. If any movement is possible, return false as the game is not over. Otherwise, return true to indicate that there are no more possible moves and that the game is over. This method should not make any changes to the game state.

You must implement this method with the given method signature and it must have the specified behavior.

REQUIRED: protected boolean slideDown ()

Slide the board down and return whether the sliding was successful (if it changed the board). This method should also increase the score for this game, if applicable.

This method will return true if sliding down is successful (i.e. the move was possible and the board changed after the sliding is completed) and return false otherwise (the board didn't change).

To slide down, there are few things you need to implement in order for the game to work.

One thing you might notice is that all the empty spaces on the board can be represented by 0s. The functionalities of movement in any direction is the same. Here is what you should implement for each column of the board to slide down.

  1. Tiles slide downward as far as they can. Remember that zeros (if you chose to represent it like this) mean there is no tile in that spot, therefore, tiles can slide past them. Tiles should stop sliding if they encounter another tile or if they encounter the edge of the board. However, if two tiles touch while sliding, then:
  2. Merge two tiles with the same number. The tiles merge into a combined tile. The combined tile's number is the sum of the two tiles. Increase the score by the combined tile's number. The combined tile should be where the bottom tile was and the top tile will become 0. This combined tile cannot merge with any other tile in the same move. Suppose for the following examples that the listed tiles are from top to bottom. Suppose we had 4 4 4. This would combine to be 0 4 8 (note that it is not 0 8 4, in other words, the farthest down tiles combine first). If we had 4 4 4 4, this would combine to be 0 0 8 8.

You must implement this method with the given method signature and it must have the specified behavior.

Here are some examples for you to visualize the basic movements of our 2048.

Example 1: Should return true

0 0 2         0 0 0
0 0 0    ->   0 0 0
0 0 0         0 0 2

Example 2: Should return true

0 0 0         0 0 0
0 0 2    ->   0 0 0 
0 0 2         0 0 4

Example 3: Should return true

0 0 2         0 0 0
0 0 2    ->   0 0 2
0 0 2         0 0 4

Example 4: Should return false

0 0 0         0 0 0
0 0 4    ->   0 0 4
0 0 8         0 0 8

An example game may be:

[cs8bwi20XX@ieng6-203]:pa4: java PlayManager
Starting a default-sized random game...
Score: 0
    -    -    -    -
    -    -    -    -
    2    -    -    2
    -    -    -    -
> d
Score: 4
    -    -    -    -
    -    -    -    -
    -    -    -    4
    -    -    2    -
> s
Score: 4
    -    -    -    -
    -    -    -    -
    -    -    -    -
    2    -    2    4
> a
Score: 8
    -    -    -    -
    -    -    -    -
    -    -    4    -
    4    4    -    -
> w
Score: 8
    4    4    4    -
    -    -    -    -
    -    -    -    -
    -    -    -    2
> a
Score: 16
    8    4    -    -
    -    -    2    -
    -    -    -    -
    2    -    -    -
> a
Score: 16
    8    4    -    -
    2    -    -    2
    -    -    -    -
    2    -    -    -
> s
Score: 20
    -    -    -    -
    -    -    -    -
    8    -    -    4
    4    4    -    2
> q

REQUIRED: public boolean move (Direction dir)

This method should slide the board in the specified direction. If the sliding is done successfully, then a new random tile must be added to the board using your addTile() method. This method should return whether the move was successful or not. If dir is null, return false without doing anything.

You must implement this method with the given method signature and it must have the specified behavior.

Note: If you implemented the rotateCounterClockwise() method then here is where its power shines. You can combine rotateCounterClockwise(), the getRotationCount() method from the Direction enum, and the slideDown() method to implement this method for all directions.

How to Play Your Game

Compile your code with javac *.java. Then, use java PlayManager to load a default game. Type one of wasd to determine the direction to slide (w is up, a is left, s is down, and d is right), o to save the game to a file called save.2048, or q to exit. You can also type java PlayManager [SAVEFILE] to load a game from [SAVEFILE].

README

The following questions about provided files and general policies are graded for fair effort and completeness. It is recommended to look at gradescope rubric to see our suggested answers afterward even if you get full credit.

  1. Describe the game of 2048 to someone that doesn't know the game. Do not use any Java or CSE terms. Write concisely as if your reader needs to read 10 summaries in a row. You might want to describe how to play the game with specific keys and what the ultimate goal is.
  2. What are the benefits of having all our magic numbers in the Config.java file? In general, what is the purpose of setting these magic numbers in a scope accessible by all your code (e.g. at the top of your file inside the class)?
  3. In GameState.java we had to return a deep copy of the board in getBoard(). Why might we want to create a deep copy rather than a shallow copy of the board if we only need read access to it?

Student Satisfaction Survey

Please fill out our student satisfaction survey. We are changing how we approach giving assignments and would like to hear about your experiences.

Style

We will grade your code style thoroughly. Namely, there are a few things you must have in each file / class / method:

  1. File header
  2. Class header
  3. Method header(s)
  4. Inline comments
  5. Proper indentation
  6. Descriptive variable names
  7. No magic numbers
  8. Reasonably short methods (if you have implemented each method according to specification in this write-up, you’re fine). This is not enforced as strictly.
  9. Lines shorter than 80 characters (keep in mind each tab is equivalent to 8 spaces).
  10. Javadoc conventions (@param, @return tags, /** comments */, etc.)

A full style guideline can be found here. If you need any clarifications, feel free to ask on Piazza.

Submission

Submit the following files to Gradescope. Make sure to wait until you see the autograder output. Required Submission Files:

  • GameState.java
  • README.md

Start early and start often!