Took quite a lot time to on this project, however I even spent more time on playing the finished game!
Introduction
The intent of this project is to give you a chance to get familiar with Java and the various tools used in the course like the IntelliJ IDE and JUnit for writing and running unit tests. Though you’ll find many files and lots of code in the proj0 folder, your task only resides in Model.java and is constrained to just four methods.
In this project, you’ll be building the core logic of this game. That is, we’ve already put together all the GUI code, handle key-presses, and a ton of other scaffolding. Your job will be to do the most important and interesting part.
Specifically, you will fill out 4 methods in the Model.java file which governs what happens after certain key-presses from the user.
The game itself is quite simple. It’s played on a 4*4 grid of squares, each of which can either be empty or contain a tile bearing an integer–a power of 2 greater than or equal to 2. Before the first move, the application adds a tile containing either 2 or 4 to a random square on the initially empty board. The choice of 2 or 4 is random, with a 75% chance of choosing 2 and a 25% chance of choosing 4.
The player then chooses a direction via their arrow keys to tilt the board: north, south, east, or west. All tiles slide in that direction until there is no empty space left in the direction of motion (there might not be any to start with). A tile can possibly merge with another tile which earns the player points.
public static boolean emptySpaceExists(Board b)
This method should return true if any of the tiles in the given board are null. You should NOT modify the Board.java file in any way for this project. For this method, you’ll want to use the tile(int col, int row) and size() methods of the Board class. No other methods are necessary.
Note: We’ve designed the Board class using a special keyword private that disallows you from using the instance variables of Board directly. For example, if you try to access b.values[0][0], this will not work. This is a good thing! It forces you to learn to use the tile method, which you’ll use throughout the rest of the project.
Try opening the TestEmptySpace.java folder. Run the tests. You should see that 6 of the tests fail and 2 of them pass. After you’ve correctly written the emptySpaceExists method, all 8 tests in TestEmptySpace should pass.
Considerations:
- The variable of the function is board b, use Board’s public method “tile”, return Tile and check if it is null;
- Double for loops to check every tile of the Board b;
- Imagine emptyspaceexists is False at start, if any tile is null, then it change to True;
- Once emptyspaceexists became true, break the two for loops.
1 | public static boolean emptySpaceExists(Board b) { |
public static boolean maxTileExists(Board b)
This method should return true if any of the tiles in the board are equal to the winning tile value 2048.
Note that rather than hard coding the constant 2048 into your code, you should use MAX_PIECE, which is a constant that is part of the Model class. In other words, you shouldn’t do if (x == 2048) but rather if (x == MAX_PIECE).
Leaving in hard coded numbers like 2048 is a bad programming practice sometimes referred to as a “magic number”. The danger of such magic numbers is that if you change them in one part of your code but not another, you might get unexpected results. By using a variable like MAX_PIECE you can ensure they all get changed together.
After you’ve written the method, the tests in TestMaxTileExists.java should pass.
Considerations:
- Should be similar to for loop each tile like the previous emptySpaceExists(Board b);
- Only tile is not null can we get the value, need to exclude null tile.
1 | public static boolean maxTileExists(Board b) { |
public static boolean atLeastOneMoveExists(Board b)
This method is more challenging. It should return true if there are any valid moves. By a “valid move”, we mean that if there is a button (UP, DOWN, LEFT, or RIGHT) that a user can press while playing 2048 that causes at least one tile to move, then such a keypress is considered a valid move.
There are two ways that there can be valid moves:
1.There is at least one empty space on the board.
2.There are two adjacent tiles with the same value.
Considerations:
- Two nested for loops to check tiles on board b;
- Set atleastonemoveexists False at start;
- The first way of valid moves: If emptyspaceexists and not maxtileexist, atleastonemoveexists is True;
- The second way of valid moves:
Should be complicated to check if two adjacent tiles with the same value;
(1)exclude the first way of valid moves;
(2)compare tile(i,j) and tile(i+1,j), if they have same value, atleastonemoveexists is True;
(3)compare tile(i,j) and tile(i,j+1), if they have same value, atleastonemoveexists is True. - Once atleastonemoveexists became true, break loops.
1 | public static boolean atLeastOneMoveExists(Board b) { |
Main Task: Building the Game Logic
public boolean tilt(Side side)
The above three methods are quite easy, the hardest part is to build the main game logic.
The tilt method does the work of actually moving all the tiles around.
In addition to modifying the board, two other things must happen:
1.The score instance variable must be updated to reflect the total value of all tile merges (if any). We merged two 4s into an 8, and two 2s into a 4, so the score should be incremented by 8 + 4 = 12.
2.If anything about the board changes, we must set the changed local variable to true. That’s because at the end of the skeleton code for tilt, you can see we call a setChanged() method: this informs the GUI that there is something to draw. You will not make any calls to setChanged yourself: only modifying the changed local variable.
All movements of tiles on the board must be completed using the move method provided by the Board class. All tiles of the board must be accessed using the tile method provided by the Board class.
Due to some details in the GUI implementation, you should only call move on a given tile once per call to tilt.
Starting by thinking only about the up direction.
Considerations:
- Write the tilt method for up direction, the skeleton gives a very cleaver way to make the other directions act according to given sides by board.setViewingPerspective(side), but reset viewing perspective to north after each move;
1
2board.setViewingPerspective(side);
board.setViewingPerspective(Side.NORTH); - Seperate into two steps;
First step:
(1) When pressing up, from the top row 3 to bottom row 0 order, use nested for loops to check each non-null tile;
(2) If there are any null space above this tile, move the tile to null tile in the highest row which the tile can reach.
Second step:
(1) After first step, check if there is additional move to merge the joint tile in same column with same value, nested for loops from the top row 3 to bottom row 0 order;
(2) Should notice that after merge, null tile between non-null tiles could be created, need to repeat the first step to fill that null space created by merge;
(3) If the upper row have merged, no further merge in this column will happen, so when we find null tile between tiles in (2), no further merge;
(4) Merge moving process: For each non-null tiles in a column, check if any previous merge, if yes, just use similar way as first step to fill null tile. if no, check if there are any joint same values;
(5) score plus 2 times of the merged tile value.
1 | public boolean tilt(Side side) { |
Enjoy the game!
Happy to see the game running and pass all tests, enjoy the game!