[Week 3] OOP game: 2048

For week 3, my goal is to make the game 2048 with Processing. I think this is a pretty suitable game to practice the various concepts from object-oriented programming, since it has various objects with the same attributes, and the different components of the game interact with one another regularly.

The game is essentially a 4×4 board with small square 1×1 tiles appearing on it, with each tile having a value which is 2 raised to some power (2, 4, 8, 16, etc.). The user can control in which direction they want to move the tiles using the right, left, up, or down keys on their keyboard. Whenever two tiles with the same value (and color) collide, they merge into a tile with double the value of each individual tile. After each move, a new tile of the lowest value (2) will appear randomly in an empty position. The goal of the game is to maximize the value on the tiles, that is, merge as many tiles as possible. The game will end when the user either wins (they reach a 2048 tile) or loses (there is no more empty space on the board and hence no further move is possible).

Here is a short demo of my program:

And here is a diagram illustrating the overall interface and outlining the basic classes of the game:

The code for the game can be found at the end of the post.

A note before starting on the details, as you can tell from the demo, I am not done with the game! The following parts outline my progress so far, and I intend to continue working on the game, so if you want to see how it turns out, here is my GitHub.

Breakdown of classes

Tile

Class Tile provides a common structure for all the tiles that will be constructed while the game runs. It has a number of attributes, but the most important ones include value (the value displaying on the tile, which determines the colors of the tile and the text), rowPos and colPos (the current coordinate of the tile on the board, with [0, 0] being the top left corner and [3, 3] being the bottom right corner; they get updated every time the tile is moved), moving and doneMoving (as the names reflect; used to initialize movement of various tiles). The class also has a number of functions, most of which are used in other classes to control the tiles’ interactions with one another and with other components of the game. The most important ones are display(), moveTileV() and moveTileH() (used to make a tile move vertically or horizontally).

Cell

The cells can be thought of as the underlying game board. They have the same layout and coordinates as the tiles, but as the tiles are initialized, displayed (and disappear) one by one as the game proceeds and move around the board, the cells are immobile. The most important attributes of a cell include occupied (boolean value to indicate whether or not there is a tile presently lying on top of the cell; it gets updated as the tiles move around), rowPos and colPos (similar to tiles), and queue (each cell has its own array to store tiles that are currently in its position; normally a cell’s queue will either be empty (unoccupied) or has one tile (occupied), so when the queue has two elements, there is a collision of tiles and they will merge). Cells have few functions among themselves, since most cell-related interactions are configured in the next class.

Grid

Each Grid object has an attribute called cells which is an array of Cell objects. My intention is to make Grid a kind of wrapper class that acts as a container for all the cells and an mediator between Cell class and Game class. For this reason, Grid is not exactly a unique class on its own, and you can see that most of the functions of this class actually call functions on cells. Some of the most usually used functions include vacant() and occupy() (used after moving existing tiles or creating new tiles to update the occupied status of a cell), enqueue() and dequeue() (to add a tile to the queue of a cell that it’s just moved to, or to remove a tile from the queue of a cell that it’s leaving, sometimes retrieving and removing the tile at once), peek() (to retrieve a tile without removing it from the queue).

Game

This is the encompassing class, wrapping and coordinating the interactions of all other classes. Each Game object has a Grid object, which has a Cell object itself. It also has 4 array containing Tile objects (4 queues). Another attribute is a HashMap called keyHandler, which maps the four key codes RIGHT, LEFT, UP, and DOWN with a boolean value to indicate which key has been pressed by the user to trigger the right movement of the tiles.

The two functions randTile() and specificTile() are used to initialized new tiles to add to the game.

There are four functions to calculate the next possible move of a tile, corresponding to which key the user has pressed: getRightDest(), getLeftDest(), getTopDest(), getBottomDest().

Using the coordinate retrieved from those functions, there are four other functions to move each individual tile: moveTileRight(), moveTileLeft(), moveTileUp(), moveTileDown(). These functions calls on various other functions and ensure interactions among the grid, the cells, and the tiles.

Before moving, the tiles are added to the four queues of the game one by one, in the order that they will be moved later. For example, if the user chooses to move all the tiles in the right direction, then the rightmost tiles in each row will be moved first, then the next tiles to their left will be moved, and so on. This is also the reason I choose the to use the queue data structure which enables the first in – first out flow. This phase of lining the tiles in queues in the four enqueueTiles() functions. Once all tiles have been added to queues in the correct order, they will in turn get retrieved and removed from the queue (still in the correct order, very important) and moved (using the moveTile() functions mentioned above). This moving phase is included in moveTilesFromQueue(). Moving one layer up, the two phases – adding tiles and moving tiles – are wrapped in the four moveGame() functions that are triggered by the key signal from the user.

Another important function is mergeTiles(), to be invoked when two same values tiles collide. I’m not done with this function yet – it partly works, as shown in the demo, but it still has a lot of bugs and pitfalls. This is also the starting point for further development of the game. Once two tiles collide and merge, they disappear and a new tile of twice the value will appear in their place, and functions from the Score class will be called on to update the score of the current game.

The Game class is missing some other important attributes and functions that I hope to incorporate: highestTile attribute and checkEnd() function to signal whether the game is won, lost, or in progress.

Score

This is the missing class! With the progress I’ve made, I haven’t constructed this class yet. Once made, the basics of the class will include the following: a currentScore attribute to store the score so far (the sum in value of all the currently visible tiles on the board) and is updated after each collision, a highestScore attribute storing, as the name suggests, the highest score so far of the current game section (each time the program is rerun is a new session).

Problems

Lots and lots of them! Most of the problems I encounter have to do with using the wrong logic and unable to detect unpredictable behavior. For such problems, I try to pinpoint the issues by adding println() statements to the functions, especially to conditional statements and loops, printing out various attributes of the objects (I still leave a number of them in my code in case I will need them again). Rather than trying to imagine what is going on, I find that printing the results out gives a much better and tangible view into the program and what possibly went wrong. For example, the following is what I print out when testing the getRightTest() function:

Whenever it is a logic problems (trying to figure out how to build the functions, etc.), I find taking notes by hands and drawing on pictures really help. Here is some of what I write and draw:

For the game, I also use a number of Java classes like HashMap, ArrayList, and ArrayDeque. I’m a bit lost deciding which ones to use, but other than that I find them all very helpful. Whenever I try to code some generic function, chances are there are already some built-in classes and data structures that handle exactly what I want to do. If you happen to suspect the same, I highly recommend doing a quick Google search. Most of the time I will search for something like “java queue oracle”, adding “oracle” so that it shows results from the Java platform API first.

My biggest takeaway from working on this game, though, has to do with a problem I had with my laptop. I wrote down my feelings on it and I’d like to quote from them:

Feb 6, 10:40 AM: I woke up to a blank Game class. It seemed I had accidentally deleted the temporary file that Processing had yet to save into the actual file. I was devastated. I was not prepared to write the entire class again. I hadn’t made a GitHub repo for the class at this point. Thank the Buddha I did have a backup file on Google Drive. After this I immediately went to push my code to GitHub. Ran into some other problems with command line along the way but it worked out. Now my code is on GitHub, as any code by any sane person should be.

In short, please back up your code.

Code

There are a lot of attributes and functions in my code that I define but never call on to use, which might be because I was working in some direction but then changed my mind and worked in a different direction. But since the game is not finished, I’m not deleting all of them yet in case I might need them when I continue with the program.

Tile class

import java.util.Map;
// https://processing.org/reference/HashMap.html

class Tile {
  int value;
  color tileColor, textColor;
  float alpha = 0;
  boolean doneDisappearing = false;
  boolean overlap = false;
  boolean visible = true;
  boolean moving = false;
  boolean doneMoving = false;
  int rowPos, colPos;
  float yPos, xPos;
  HashMap<Integer, Integer> tileColorMap = new HashMap<Integer, Integer>();
  int tileSize = 100;
  int tileRound = 7;
  int gapSize = 12;
  float xOffset = width/2 - (gapSize*2.5 + tileSize*2);
  // yOffset might + extra offset to account for the score part
  float yOffset = height/2 - (gapSize*2.5 + tileSize*2);
  int gridNum = 4;
  int tileID;

  /**
   * Map tile values with corresponding colors in pairs of <Value, Color>
   * Note: wrapper class of color primitive type is Integer
   */
  void mapColors() {
    tileColorMap.put(2, color(238, 228, 218, alpha));
    tileColorMap.put(4, color(237, 224, 200, alpha));
    tileColorMap.put(8, color(242, 177, 121, alpha));
    tileColorMap.put(16, color(245, 149, 99, alpha));
    tileColorMap.put(32, color(246, 124, 96, alpha));
    tileColorMap.put(64, color(246, 94, 59, alpha));
    tileColorMap.put(128, color(237, 207, 115, alpha));
    tileColorMap.put(256, color(237, 204, 98, alpha));
    tileColorMap.put(512, color(237, 200, 80, alpha));
    tileColorMap.put(1024, color(237, 197, 63, alpha));
    tileColorMap.put(2048, color(237, 194, 45, alpha));
  }

  /**
   * Constructor of a tile
   * @param val value of the tile, must be of a value 2^n with 1 <= n <= 11
   *        row, col coordinate of the tile
   * [0, 0] is top left, [3, 3] is bottom right
   */
  Tile(int val, int row, int col) {
    value = val;
    rowPos = row;
    colPos = col;
    xPos = xPos(col);
    yPos = yPos(row);
    tileID = gridNum*row+col;
  }

  /**
   * Display a particular tile. Necessary params are all class attributes
   */
  void displayTile() {
    // Alpha might get updated so need to map colors again
    mapColors();
    tileColor = tileColorMap.get(value);
    // There are only two colors for text so no need for HashMap
    if (value <= 4) {
      textColor = color(119, 110, 101, alpha);
    } else {
      textColor = color(249, 246, 242, alpha);
    }

    updateAlpha();

    pushStyle();
    rectMode(CENTER);
    noStroke();
    fill(tileColor);
    rect(xPos, yPos, tileSize, tileSize, tileRound);
    fill(textColor);
    textSize(tileSize*.4);
    // tileSize*.5 is too big -- 4-digit values do not fit
    textAlign(CENTER, CENTER);
    text(value, xPos, yPos-textAscent()*.1);
    popStyle();
  }

  /**
   * Move tile horizontally to a new position
   * @param destCol column-coordinate of destination
   */
  void moveTileH(int destCol) {
    float oldX = xPos(colPos);
    float newX = xPos(destCol);
    float stepSize = (newX-oldX)/15;
    if (xPos != newX) {
      xPos += stepSize;
    }
    // stepSize can have long decimal part
    // Without rounding, xPos can offshoot newX by a fraction
    if (round(xPos) == newX) {
      xPos = newX;
      colPos = destCol;
      moving = false;
      doneMoving = true;
      updateID();
    }
  }

  /**
   * Move tile vertically to a new position
   * @param destRow row-coordinate of destination
   */
  void moveTileV(int destRow) {
    float oldY = yPos(rowPos);
    float newY = yPos(destRow);
    float stepSize = (newY-oldY)/15;
    if (yPos != newY) {
      yPos += stepSize;
    }
    if (round(yPos) == newY) {
      yPos = newY;
      rowPos = destRow;
      moving = false;
      doneMoving = true;
      updateID();
    }
  }

  /**
   * Change transparency of a tile. Used to make tile (dis)appear
   */
  void updateAlpha() {
    if (visible) {
      if (alpha < 255) {
        alpha += 17; // 17=255/15 (15, 30, 60 ...)
      } else {
        alpha = 255;
      }
      alpha = 255;
    } else {
      if (alpha > 0) {
        alpha -= 17;
      } else {
        alpha = 0;
      }
    }
  }

  /**
   * Update visibility of a tile to trigger its disappearing
   */
  void disappear() {
    visible = false;
  }

  /**
   * Update ID of a recently moved tile
   * So far not used much because it's too simple a calculation
   * I forget I made a function
   */
  void updateID() {
    tileID = gridNum*rowPos+colPos;
  }

  /**
   * Return the x-coordinate of a tile in pixels
   * @param colPos column coordinate (0, 1, 2, 3)
   * @return xPos corresponding x-coordinate in pixels
   */
  float xPos(int colPos) {
    return (colPos+.5)*tileSize + (colPos+1)*gapSize + xOffset;
  }

  /**
   * Return the y-coordinate of a tile in pixels
   * @param rowPos row coordinate (0, 1, 2, 3)
   * @return yPos corresponding y-coordinate in pixels
   */
  float yPos(int rowPos) {
    return (rowPos+.5)*tileSize + (rowPos+1)*gapSize + yOffset;
  }

  /**
   * Check if this tile and another tile has the same value
   * @param tile a second tile to compare
   * @return true if same value
   */
  boolean sameValue(Tile tile) {
    if (value == tile.value) {
      return true;
    } else {
      return false;
    }
  }

  /**
   * Checks if this tile is at the top edge aka first row
   * @return true if it is at the top edge
   */
  boolean isAtTopEdge() {
    if (rowPos == 0) { 
      return true;
    } else {
      return false;
    }
  }

  /**
   * Checks if this tile is at the bottom edge aka last row
   * @return true if it is at the bottom edge
   */
  boolean isAtBottomEdge() {
    if (rowPos == 3) { 
      return true;
    } else {
      return false;
    }
  }

  /**
   * Checks if this tile is at the left edge aka first column
   * @return true if it is at the left edge
   */
  boolean isAtLeftEdge() {
    if (colPos == 0) { 
      return true;
    } else {
      return false;
    }
  }

  /**
   * Checks if this tile is at the right edge aka last column
   * @return true if it is at the right edge
   */
  boolean isAtRightEdge() {
    if (colPos == 3) { 
      return true;
    } else {
      return false;
    }
  }

  /**
   * Reset movement attributes of the tile
   * aka not moving & hasn't started moving
   */
  void resetMovement() {
    moving = false;
    doneMoving = false;
  }

  /**
   * Misc code for testing purposes
   */
  void test() {
    println("Row: "+str(rowPos));
    println("Col: "+str(colPos));
  }
}

Cell class

import java.util.ArrayDeque;
// https://docs.oracle.com/javase/9/docs/api/java/util/ArrayDeque.html

class Cell {
  int gridNum = 4;
  int cellID;
  int rowPos, colPos;
  float yPos, xPos;
  color cellColor = color(205, 191, 180, 255);
  boolean occupied = false;
  int cellSize = 100;
  int cellRound = 7;
  int gapSize = 12;
  float xOffset;
  float yOffset;
  int count = 0;
  ArrayDeque<Tile> queue = new ArrayDeque();

  /**
   * Constructor of a cell
   * @param row, col coordinate of the cell
   *        offset_x, offset_y offets to align the game board
   */
  Cell(int row, int col, float offset_x, float offset_y) {
    rowPos = row;
    colPos = col;
    xOffset = offset_x;
    yOffset = offset_y;
    xPos = xPos(col);
    yPos = yPos(row);
    cellID = gridNum*row+col;
  }

  /**
   * Display the cell
   * I'm warry of unpredicted behavior hence pushStyle() popStyle()
   */
  void displayCell() {
    pushStyle();
    rectMode(CENTER);
    noStroke();
    fill(cellColor);
    rect(xPos, yPos, cellSize, cellSize, cellRound);
    popStyle();
  }

  /**
   * Return the x-coordinate of a cell in pixels
   * @param colPos column coordinate (0, 1, 2, 3)
   * @return xPos corresponding x-coordinate in pixels
   */
  float xPos(int colPos) {
    return (colPos+.5)*cellSize + (colPos+1)*gapSize + xOffset;
  }

  /**
   * Return the y-coordinate of a cell in pixels
   * @param colPos column coordinate (0, 1, 2, 3)
   * @return yPos corresponding y-coordinate in pixels
   */
  float yPos(int rowPos) {
    return (rowPos+.5)*cellSize + (rowPos+1)*gapSize + yOffset;
  }
}

Grid class

class Grid {
  float gridSize = 460;
  int gridNum = 4;
  int gridRound = 7;
  int cellSize = 100;
  int gapSize = 12;
  ArrayList<Cell> cells = new ArrayList<Cell>();
  float xOffset = width/2 - (gapSize*2.5 + cellSize*2);
  // yOffset might + extra offset to account for the score part
  float yOffset = height/2 - (gapSize*2.5 + cellSize*2);

  /**
   * Constructor of a grid
   */
  Grid() {
    for (int i=0; i<gridNum; i++) {
      for (int j=0; j<gridNum; j++) {
        Cell cell = new Cell(i, j, xOffset, yOffset);
        cells.add(cell);
      }
    }
  }

  /**
   * Change the size of a grid
   * To be honest I forgot I ever made this at all
   * Might find some use for it?
   */
  void setGridSize(int size) {
    gridSize = size;
  }

  /**
   * Display the background and the cells
   */
  void displayGrid() {
    displayBg();
    for (int i=0; i<cells.size(); i++) {
      Cell cell = cells.get(i);
      cell.displayCell();
    }
  }

  /**
   * Display the background
   * Once again I'm using pushStyle() popStyle() to be sure
   */
  void displayBg() {
    color bgColor = color(187, 172, 160, 255);
    pushStyle();
    fill(bgColor);
    noStroke();
    rectMode(CENTER);
    rect(width/2, height/2, gridSize, gridSize, gridRound);
    popStyle();
  }

  /**
   * Check if a cell is occupied
   * Though I don't think these params are ever used
   * @param row, col coordinate of the cell
   * @return true if occupied
   */
  boolean checkIfOccupied(int row, int col) {
    int id = getID(row, col);
    Cell cell = cells.get(id);
    return cell.occupied;
  }

  /**
   * Check if a cell is occupied
   * @param id ID of the cell
   * @return true if occupied
   */
  boolean checkIfOccupied(int id) {
    Cell cell = cells.get(id);
    return cell.occupied;
  }

  //void changeStatus(Tile tile) {
  //  int id = getID(tile.rowPos, tile.colPos);
  //  Cell cell = cells.get(id);
  //  cell.changeStatus();
  //}

  /**
   * Change an occupied cell to unoccupied
   * @param tile the tile to be moved or disappear
   */
  void vacant(Tile tile) {
    int id = getID(tile.rowPos, tile.colPos);
    Cell cell = cells.get(id);
    cell.occupied = false;
  }

  /**
   * Change an occupied cell to unoccupied
   * @param id ID of the tile to be moved away or disappear
   *           and/or cell to be cleared
   */
  void vacant(int id) {
    Cell cell = cells.get(id);
    cell.occupied = false;
  }

  /**
   * Change an unoccupied cell to occupied
   * @param tile the tile to be moved here or appear
   */
  void occupy(Tile tile) {
    int id = getID(tile.rowPos, tile.colPos);
    Cell cell = cells.get(id);
    cell.occupied = true;
  }

  /**
   * Change an unoccupied cell to occupied
   * @param id ID of the tile to be moved here or appear
   *           and/or cell to be occupied
   */
  void occupy(int id) {
    Cell cell = cells.get(id);
    cell.occupied = true;
  }

  /**
   * Add to the count of the cell (how many tiles are in it)
   * Cell's count attribute is actually kinda redundant.
   * Only needs to check the size of the queue.
   * @param tile the tile at the coordinate to be checked
   */
  void addCount(Tile tile) {
    int id = getID(tile.rowPos, tile.colPos);
    Cell cell = cells.get(id);
    cell.count += 1;
  }

  /**
   * Minus from the count of the cell
   * @param tile the tile at the coordinate to be checked
   */
  void minusCount(Tile tile) {
    int id = getID(tile.rowPos, tile.colPos);
    Cell cell = cells.get(id);
    cell.count -= 1;
  }

  /**
   * Add a tile to (the end of) a cell's queue
   * @param tile tile to be added
   */
  void enqueue(Tile tile) {
    int id = getID(tile.rowPos, tile.colPos);
    Cell cell = cells.get(id);
    cell.queue.add(tile);
  }

  /**
   * Retrieve and remove a tile from (the head of) a cell's queue
   * @param tile tile at the cell in question
   * @return tile at the head of the cell's queue
   */
  Tile dequeue(Tile tile) {
    int id = getID(tile.rowPos, tile.colPos);
    Cell cell = cells.get(id);
    return cell.queue.remove();
  }

  /**
   * Retrieve and remove a tile from (the head of) a cell's queue
   * @param id coordinate of the cell in question
   * @return tile at the head of the cell's queue
   */
  Tile dequeue(int id) {
    Cell cell = cells.get(id);
    return cell.queue.remove();
  }

  /**
   * Retrieve without removing a tile from the head of a cell's queue
   * @param id coordinate of the cell in question
   * @return tile at the head of the cell's queue
   */
  Tile peekFirst(int id) {
    Cell cell = cells.get(id);
    return cell.queue.peekFirst();
  }

  /**
   * Retrieve without removing a tile from the end of a cell's queue
   * @param id coordinate of the cell in question
   * @return tile at the end of the cell's queue
   */
  Tile peekLast(int id) {
    Cell cell = cells.get(id);
    return cell.queue.peekLast();
  }

  /**
   * Retrieve without removing a tile from the head of a cell's queue
   * I've just realized it's equivalent to peekFirst()
   * @param id coordinate of the cell in question
   * @return tile at the head of the cell's queue
   */
  Tile peek(int id) {
    Cell cell = cells.get(id);
    return cell.queue.peek();
  }

  /**
   * Return the current size of a cell's queue
   * aka how many tiles it currently contains
   * @param id coordinate of the cell to check
   * @return size of cell's queue
   */
  int queueSize(int id) {
    Cell cell = cells.get(id);
    return cell.queue.size();
  }

  /**
   * Check if a collision is happenning at a cell
   * Can be modified to get rid of count attribute
   * @param tile tile at the cell to check
   * @return true if a cell currently contains >1 tiles
   */
  boolean checkCollision(Tile tile) {
    int id = getID(tile.rowPos, tile.colPos);
    Cell cell = cells.get(id);
    if (cell.count == 2) {
      return true;
    } else {
      return false;
    }
  }

  /**
   * Check if a collision is happenning at any cell
   * @return true if any cell currently contains >1 tiles
   */
  boolean checkCollision() {
    for (int i=0; i<cells.size(); i++) {
      Cell cell = cells.get(i);
      if (cell.queue.size() == 2) {
        return true;
      }
    }
    return false;
  }

  /**
   * Return the coordinate of the cell with a collision
   * Only gets revoked when there is a collision anywhere in the board
   * @return i id of the first cell with a collision
   */
  int whereCollision() {
    for (int i=0; i<cells.size(); i++) {
      Cell cell = cells.get(i);
      if (cell.queue.size() == 2) {
        println("Collision in: "+str(i));
        return i;
      }
    }
    return -1;
  }
  /**
   * Return tile/cell ID corresponding to a coordinate
   * Rarely used, calling it is just as long as writing the code
   * @param row, col coordinate
   */
  int getID(int row, int col) {
    return gridNum*row+col;
  }
  
  /**
   * Misc code for testing purposes
   */
  void occHelper() {
    for (int i=0; i<4; i++) {
      for (int j=0; j<4; j++) {
        if (checkIfOccupied(i, j)) {
          println(i, j);
        }
      }
    }
  }
}

Game class

import java.util.Map;
import java.util.ArrayDeque;

class Game {
  Grid grid = new Grid();
  int gridNum = 4;
  HashMap<Integer, Boolean> keyHandler = new HashMap<Integer, Boolean>();
  int numTiles = 16;
  ArrayDeque<Tile> queue0 = new ArrayDeque();
  ArrayDeque<Tile> queue1 = new ArrayDeque();
  ArrayDeque<Tile> queue2 = new ArrayDeque();
  ArrayDeque<Tile> queue3 = new ArrayDeque();

  /**
   * Constructor of a game
   * Reset keyHandler = no arrow key has been pressed
   * Randomize a tile
   */
  Game() {
    keyHandler.put(LEFT, false);
    keyHandler.put(RIGHT, false);
    keyHandler.put(UP, false);
    keyHandler.put(DOWN, false);

    randTile();
  }

  /**
   * The main proceedings of the game
   */
  void displayGame() {
    grid.displayGrid();
    displayTiles();

    // For each loop check if an arrow is pressed
    // & revoke the corresponding game movement
    if (keyHandler.get(RIGHT)) {
      moveGameRight();
    } else if (keyHandler.get(LEFT)) {
      moveGameLeft();
    } else if (keyHandler.get(UP)) {
      moveGameUp();
    } else if (keyHandler.get(DOWN)) {
      moveGameDown();
    }
    if (allDoneMoving()) {
      // This works but in the case of 2 - 2 - 2 for example it's wrong 
      checkAndMerge();

      randTile();
    }
  }

  /**
   * Check the entire game board to detect a collision
   * then merge the tiles that collided
   * A bit exhaustive to check every cell but not too taxing overall
   */
  void checkAndMerge() {
    for (int i=0; i<numTiles; i++) {
      if (grid.queueSize(i) == 2) {
        mergeTiles(i);
      }
    }
  }

  /**
   * Merge collided tiles
   * Still has a lot of bugs e.g. 3 same tiles on a row/col
   * @param id ID of the cell where the collision is
   */
  void mergeTiles(int id) {
    int value = grid.peek(id).value*2;
    int row = grid.peek(id).rowPos;
    int col = grid.peek(id).colPos;
    grid.dequeue(id);
    grid.dequeue(id);
    specificTile(value, row, col);
  }

  /**
   * Display all the valid tiles
   * Valid tiles: visible, no colliding yet
   */
  void displayTiles() {
    // For testing (console printing)
    int validTiles = 0;
    for (int i=0; i<numTiles; i++) {
      if (grid.checkIfOccupied(i)) {
        Tile curTile = grid.peek(i);
        validTiles += 1;
        curTile.displayTile();
      }
    }
  }

  /**
   * Add tiles to the four queues for moving to the right
   * @param queue the queue to be added to
   *        row the row to consider (0, 1, 2, 3)
   */
  void enqueueTilesFromRight(ArrayDeque<Tile> queue, int row) {
    queue.clear();
    // Rightmost tiles to be added (and later retrieved) first
    // e.g. [_][2][4][_]
    //      -0--1--2--3-
    //      Tile at -2- needs to move to -3- first
    //      so -2- is made vacant, -3- is made occupied
    //      When -1- moves -2- is now free
    for (int i=3; i>=0; i--) {
      int id = grid.getID(row, i);
      if (grid.checkIfOccupied(id)) {
        Tile curTile = grid.peek(id);
        queue.add(curTile);
      }
    }
  }

  /**
   * Add tiles to the four queues for moving to the left
   * @param queue the queue to be added to
   *        row the row to consider (0, 1, 2, 3)
   */
  void enqueueTilesFromLeft(ArrayDeque<Tile> queue, int row) {
    queue.clear();
    // Leftmost tiles to be added (and later retrieved) first
    // see enqueueTilesFromRight() for example, same logic
    for (int i=0; i<4; i++) {
      int id = grid.getID(row, i);
      if (grid.checkIfOccupied(id)) {
        Tile curTile = grid.peek(id);
        queue.add(curTile);
      }
    }
  }

  /**
   * Add tiles to the four queues for moving up
   * @param queue the queue to be added to
   *        col the col to consider (0, 1, 2, 3)
   */
  void enqueueTilesFromTop(ArrayDeque<Tile> queue, int col) {
    queue.clear();
    // Top tiles to be added (and later retrieved) first
    // see enqueueTilesFromRight() for example, same logic
    for (int i=0; i<4; i++) {
      int id = grid.getID(i, col);
      if (grid.checkIfOccupied(id)) {
        Tile curTile = grid.peek(id);
        queue.add(curTile);
      }
    }
  }

  /**
   * Add tiles to the four queues for moving down
   * @param queue the queue to be added to
   *        col the col to consider (0, 1, 2, 3)
   */
  void enqueueTilesFromBottom(ArrayDeque<Tile> queue, int col) {
    queue.clear();
    // Bottom tiles to be added (and later retrieved) first
    // see enqueueTilesFromRight() for example, same logic
    for (int i=3; i>=0; i--) {
      int id = grid.getID(i, col);
      if (grid.checkIfOccupied(id)) {
        Tile curTile = grid.peek(id);
        queue.add(curTile);
      }
    }
  }

  /**
   * Retrieve tiles from queues to move
   * @param queue the queue to be retrieved tiles from
   *        dir current moving direction
   */
  void moveTilesFromQueue(ArrayDeque<Tile> queue, String dir) {
    // Movement of a tile is triggered when 
    // the previous tile has finished moving
    // but the first tile in queue has no such
    // anchor point to compare to, so it needs
    // to be retrieved and moved on its own.
    if (!queue.isEmpty()) {
      boolean lastTileDone = false;
      int sizeQueue = queue.size();
      Tile curTile = queue.remove();
      if (dir == "RIGHT") {
        moveTileRight(curTile);
      } else if (dir == "LEFT") {
        moveTileLeft(curTile);
      } else if (dir == "UP") {
        moveTileUp(curTile);
      } else if (dir == "DOWN") {
        moveTileDown(curTile);
      }
      if (curTile.doneMoving) {
        lastTileDone = true;
      }
      // Then for each remaining tiles,
      // retrieve and move them
      for (int i=0; i<sizeQueue-1; i++) {
        if (lastTileDone == true) {
          curTile = queue.remove();
          if (dir == "RIGHT") {
            moveTileRight(curTile);
          } else if (dir == "LEFT") {
            moveTileLeft(curTile);
          } else if (dir == "UP") {
            moveTileUp(curTile);
          } else if (dir == "DOWN") {
            moveTileDown(curTile);
          }
          if (!curTile.doneMoving) {
            lastTileDone = false;
          }
        }
      }
    }
  }

  /**
   * Move all tiles on board to the right
   * After all have finished moving
   * lock all keyHandler values, i.e.
   * no direction is currently chosen
   */
  void moveGameRight() {
    enqueueTilesFromRight(queue0, 0);
    enqueueTilesFromRight(queue1, 1);
    enqueueTilesFromRight(queue2, 2);
    enqueueTilesFromRight(queue3, 3);
    moveTilesFromQueue(queue0, "RIGHT");
    moveTilesFromQueue(queue1, "RIGHT");
    moveTilesFromQueue(queue2, "RIGHT");
    moveTilesFromQueue(queue3, "RIGHT");
    lockMovement();
  }

  /**
   * Move all tiles on board to the left
   */
  void moveGameLeft() {
    enqueueTilesFromLeft(queue0, 0);
    enqueueTilesFromLeft(queue1, 1);
    enqueueTilesFromLeft(queue2, 2);
    enqueueTilesFromLeft(queue3, 3);
    moveTilesFromQueue(queue0, "LEFT");
    moveTilesFromQueue(queue1, "LEFT");
    moveTilesFromQueue(queue2, "LEFT");
    moveTilesFromQueue(queue3, "LEFT");
    lockMovement();
  }

  /**
   * Move all tiles on board up
   */
  void moveGameUp() {
    enqueueTilesFromTop(queue0, 0);
    enqueueTilesFromTop(queue1, 1);
    enqueueTilesFromTop(queue2, 2);
    enqueueTilesFromTop(queue3, 3);
    moveTilesFromQueue(queue0, "UP");
    moveTilesFromQueue(queue1, "UP");
    moveTilesFromQueue(queue2, "UP");
    moveTilesFromQueue(queue3, "UP");
    lockMovement();
  }

  /**
   * Move all tiles on board down
   */
  void moveGameDown() {
    enqueueTilesFromBottom(queue0, 0);
    enqueueTilesFromBottom(queue1, 1);
    enqueueTilesFromBottom(queue2, 2);
    enqueueTilesFromBottom(queue3, 3);
    moveTilesFromQueue(queue0, "DOWN");
    moveTilesFromQueue(queue1, "DOWN");
    moveTilesFromQueue(queue2, "DOWN");
    moveTilesFromQueue(queue3, "DOWN");
    lockMovement();
  }

  /**
   * Check if all the tiles have done moving
   * @return false if any tile is not done
   */
  boolean allDoneMoving() {
    for (int i=0; i<numTiles; i++) {
      if (grid.checkIfOccupied(i)) {
        Tile curTile = grid.peek(i);
        if (!curTile.doneMoving) {
          return false;
        }
      }
    }
    return true;
  }

  /**
   * Set all keyHandler values to false
   * aka no (new) direction has been chosen.
   */
  void lockMovement() {
    if (allDoneMoving()) {
      keyHandler.put(RIGHT, false);
      keyHandler.put(LEFT, false);
      keyHandler.put(UP, false);
      keyHandler.put(DOWN, false);
    }
  }

  /**
   * Randomize a new tile of value 2
   */
  void randTile() {
    // Randomize a new coordinate
    // until an empty cell is found.
    int randRow = floor(random(4));
    int randCol = floor(random(4));
    while (grid.checkIfOccupied(randRow, randCol)) {
      randRow = floor(random(4));
      randCol = floor(random(4));
    }
    Tile tile = new Tile(2, randRow, randCol);
    grid.occupy(tile);
    grid.addCount(tile);
    grid.enqueue(tile);
  }

  /**
   * Add a new tile with specified value and coordinate
   * @param val value to be set to tile
   *        row, col coordinate of tile
   */
  void specificTile(int val, int row, int col) {
    Tile tile = new Tile(val, row, col);
    grid.occupy(tile);
    grid.addCount(tile);
    grid.enqueue(tile);
  }

  /**
   * Move a tile to the rightmost valid destination
   * @param tile tile to be moved
   */
  void moveTileRight(Tile tile) {
    tile.resetMovement();
    grid.vacant(tile);
    grid.minusCount(tile);
    grid.dequeue(tile);
    tile.moving = true;
    int dest = tile.colPos;
    if (tile.moving && !tile.doneMoving) {
      dest = getRightDest(tile);
    }
    tile.moveTileH(dest);
    grid.occupy(tile);
    grid.addCount(tile);
    grid.enqueue(tile);
  }

  /**
   * Move a tile to the leftmost valid destination
   * @param tile tile to be moved
   */
  void moveTileLeft(Tile tile) {
    tile.resetMovement();
    grid.vacant(tile);
    grid.minusCount(tile);
    grid.dequeue(tile);
    tile.moving = true;
    int dest = tile.colPos;
    if (tile.moving && !tile.doneMoving) {
      dest = getLeftDest(tile);
    }
    tile.moveTileH(dest);
    grid.occupy(tile);
    grid.addCount(tile);
    grid.enqueue(tile);
  }

  /**
   * Move a tile to the top valid destination
   * @param tile tile to be moved
   */
  void moveTileUp(Tile tile) {
    tile.resetMovement();
    grid.vacant(tile);
    grid.minusCount(tile);
    grid.dequeue(tile);
    tile.moving = true;
    int dest = tile.rowPos;
    if (tile.moving && !tile.doneMoving) {
      dest = getTopDest(tile);
    }
    tile.moveTileV(dest);
    grid.occupy(tile);
    grid.addCount(tile);
    grid.enqueue(tile);
  }

  /**
   * Move a tile to the bottom valid destination
   * @param tile tile to be moved
   */
  void moveTileDown(Tile tile) {
    tile.resetMovement();
    grid.vacant(tile);
    grid.minusCount(tile);
    grid.dequeue(tile);
    tile.moving = true;
    int dest = tile.rowPos;
    if (tile.moving && !tile.doneMoving) {
      dest = getBottomDest(tile);
    }
    tile.moveTileV(dest);
    grid.occupy(tile);
    grid.addCount(tile);
    grid.enqueue(tile);
  }

  /**
   * Return coordinate of the rightmost valid destination for a tile
   * @param tile tile to consider
   * @return col column coordinate of destination cell
   */
  int getRightDest(Tile tile) {
    if (tile.isAtRightEdge()) {
      // If current cell is already at right edge, don't move it
      // aka destination = itself
      //println("Tile at ["+str(tile.rowPos)+", "+str(tile.colPos)+"] is at right edge.");
      return tile.colPos;
    } else {
      // If it is not at edge, consider the tile to the right
      //println("Tile at ["+str(tile.rowPos)+", "+str(tile.colPos)+"] is NOT at right edge.");
      int curCol = tile.colPos + 1;
      while (curCol <= 3) {
        // Keep checking further (within board) until an invalid destination
        //println("Checking tile at ["+str(tile.rowPos)+", "+str(curCol)+"]");
        if (grid.checkIfOccupied(tile.rowPos, curCol)) {
          // If the current cell has another tile, check if two tiles have the same value
          // If they do they can be in the same cell (collision & merging happens)
          // If they don't then move back to the last valid destination
          //println("Tile at ["+str(tile.rowPos)+", "+str(curCol)+"] is occupied.");
          int rightTileID = gridNum*tile.rowPos + curCol;
          //Tile rightTile = tiles[rightTileID];
          Tile rightTile = grid.peek(rightTileID);
          if (tile.sameValue(rightTile)) {
            //println("Tile at ["+str(rightTile.rowPos)+", "+str(rightTile.colPos)+"] has the same value.");
            tile.overlap = true;
            return curCol; // ADDED -1 FOR TESTING
          } else {
            //println("Tile at ["+str(rightTile.rowPos)+", "+str(rightTile.colPos)+"] has different value.");
            return curCol-1;
          }
        } else {
          // If the current cell is free, check further
          //println("Tile at ["+str(tile.rowPos)+", "+str(curCol)+"] is NOT occupied.");
          curCol += 1;
        }
      }
      return curCol-1;
    }
  }

  int getLeftDest(Tile tile) {
    // See getRightDest(), same logic
    if (tile.isAtLeftEdge()) {
      //println("Tile at ["+str(tile.rowPos)+", "+str(tile.colPos)+"] is at left edge.");
      return tile.colPos;
    } else {
      //println("Tile at ["+str(tile.rowPos)+", "+str(tile.colPos)+"] is NOT at left edge.");
      int curCol = tile.colPos - 1;
      while (curCol >= 0) {
        //println("Checking tile at ["+str(tile.rowPos)+", "+str(curCol)+"]");
        if (grid.checkIfOccupied(tile.rowPos, curCol)) {
          //println("Tile at ["+str(tile.rowPos)+", "+str(curCol)+"] is occupied.");
          int leftTileID = gridNum*tile.rowPos + curCol;
          //Tile leftTile = tiles[leftTileID];
          Tile leftTile = grid.peek(leftTileID);
          if (tile.sameValue(leftTile)) {
            //println("Tile at ["+str(leftTile.rowPos)+", "+str(leftTile.colPos)+"] has the same value.");
            return curCol; // ADDED +1 FOR TESTING
          } else {
            //println("Tile at ["+str(leftTile.rowPos)+", "+str(leftTile.colPos)+"] has different value.");
            return curCol+1;
          }
        } else {
          //println("Tile at ["+str(tile.rowPos)+", "+str(curCol)+"] is NOT occupied.");
          curCol -= 1;
        }
      }
      return curCol+1;
    }
  }

  int getTopDest(Tile tile) {
    // See getRightDest(), same logic
    if (tile.isAtTopEdge()) {
      //println("Tile at ["+str(tile.rowPos)+", "+str(tile.colPos)+"] is at top edge.");
      return tile.rowPos;
    } else {
      //println("Tile at ["+str(tile.rowPos)+", "+str(tile.colPos)+"] is NOT at top edge.");
      int curRow = tile.rowPos - 1;
      while (curRow >= 0) {
        //println("Checking tile at ["+str(tile.rowPos)+", "+str(curCol)+"]");
        if (grid.checkIfOccupied(curRow, tile.colPos)) {
          //println("Tile at ["+str(curRow)+", "+str(tile.colPos)+"] is occupied.");
          int topTileID = gridNum*curRow + tile.colPos;
          //Tile topTile = tiles[topTileID];
          Tile topTile = grid.peek(topTileID);
          if (tile.sameValue(topTile)) {
            //println("Tile at ["+str(topTile.rowPos)+", "+str(topTile.colPos)+"] has the same value.");
            return curRow; // ADDED +1 FOR TESTING
          } else {
            //println("Tile at ["+str(topTile.rowPos)+", "+str(topTile.colPos)+"] has different value.");
            return curRow+1;
          }
        } else {
          //println("Tile at ["+str(curRow)+", "+str(tile.colPos)+"] is NOT occupied.");
          curRow -= 1;
        }
      }
      return curRow+1;
    }
  }

  int getBottomDest(Tile tile) {
    // See getRightDest(), same logic
    //println("Moving tile at ["+str(tile.rowPos)+", "+str(tile.colPos)+"]");
    if (tile.isAtBottomEdge()) {
      //println("Tile at ["+str(tile.rowPos)+", "+str(tile.colPos)+"] is at bottom edge.");
      //println("return"+str(tile.rowPos));
      return tile.rowPos;
    } else {
      //println("Tile at ["+str(tile.rowPos)+", "+str(tile.colPos)+"] is NOT at bottom edge.");
      int curRow = tile.rowPos + 1;
      while (curRow <= 3) {
        //println("Checking tile at ["+str(curRow)+", "+str(tile.colPos)+"]");
        if (grid.checkIfOccupied(curRow, tile.colPos)) {
          //println("Tile at ["+str(curRow)+", "+str(tile.colPos)+"] is occupied.");
          int bottomTileID = gridNum*curRow + tile.colPos;
          //Tile bottomTile = tiles[bottomTileID];
          Tile bottomTile = grid.peek(bottomTileID);
          if (tile.sameValue(bottomTile)) {
            //println("Tile at ["+str(bottomTile.rowPos)+", "+str(bottomTile.colPos)+"] has the same value.");
            //println("return"+str(curRow));
            return curRow; // ADDED -1 FOR TESTING
          } else {
            //println("Tile at ["+str(bottomTile.rowPos)+", "+str(bottomTile.colPos)+"] has different value.");
            //println("return"+str(curRow-1));
            return curRow-1;
          }
        } else {
          //println("Tile at ["+str(curRow)+", "+str(tile.colPos)+"] is NOT occupied.");
          curRow += 1;
        }
      }
      //println("return"+str(curRow-1));
      return curRow-1;
    }
  }
}

Main program

Tile tile0, tile1, tile2, tile3, tile4;
ArrayList<Tile> tiles = new ArrayList<Tile>();
Cell cell0, cell1, cell2, cell3;
ArrayList<Cell> cells = new ArrayList<Cell>();
Game game;

void setup() {
  size(600, 600);
  game = new Game();
}

void draw() {
  background(255);
  testGame();
}

void testGame() {
  game.displayGame();
}

void testGrid() {
  Grid grid = new Grid();
  grid.displayGrid();
}

void testCell() {
  for (int i=0; i<cells.size(); i++) {
    Cell cell = cells.get(i);
    cell.displayCell();
  }
}

void testTile() {
  for (int i=0; i<tiles.size(); i++) {
    Tile tile = tiles.get(i);
    tile.displayTile();
  }
}

void keyPressed() {
  if (key == CODED) {
    if (keyCode == RIGHT) {
      game.keyHandler.replace(RIGHT, true);
      game.keyHandler.replace(LEFT, false);
      game.keyHandler.replace(UP, false);
      game.keyHandler.replace(DOWN, false);
    }
    if (keyCode == LEFT) {
      game.keyHandler.replace(RIGHT, false);
      game.keyHandler.replace(LEFT, true);
      game.keyHandler.replace(UP, false);
      game.keyHandler.replace(DOWN, false);
    }
    if (keyCode == UP) {
      game.keyHandler.replace(RIGHT, false);
      game.keyHandler.replace(LEFT, false);
      game.keyHandler.replace(UP, true);
      game.keyHandler.replace(DOWN, false);
    }
    if (keyCode == DOWN) {
      game.keyHandler.replace(RIGHT, false);
      game.keyHandler.replace(LEFT, false);
      game.keyHandler.replace(UP, false);
      game.keyHandler.replace(DOWN, true);
    }
  }
}

 

Leave a Reply