2048 / src / game2048 / Model.java
Model.java
Raw
package game2048;

import java.util.Formatter;
import java.util.Observable;


/** The state of a game of 2048.
 *  @author Bassem Halim
 */

/**
 * board orientation
 * 3|
 * 2|
 * 1|
 * 0|__________
 *   0 1 2 3
 */
public class Model extends Observable {
    /** Current contents of the board. */
    private Board board;
    /** Current score. */
    private int score;
    /** Maximum score so far.  Updated when game ends. */
    private int maxScore;
    /** True iff game is ended. */
    private boolean gameOver;

    /* Coordinate System: column C, row R of the board (where row 0,
     * column 0 is the lower-left corner of the board) will correspond
     * to board.tile(c, r).  Be careful! It works like (x, y) coordinates.
     */

    /** Largest piece value. */
    public static final int MAX_PIECE = 2048;

    /** A new 2048 game on a board of size SIZE with no pieces
     *  and score 0. */
    public Model(int size) {
        board = new Board(size);
        score = maxScore = 0;
        gameOver = false;
    }

    /** A new 2048 game where RAWVALUES contain the values of the tiles
     * (0 if null). VALUES is indexed by (row, col) with (0, 0) corresponding
     * to the bottom-left corner. Used for testing purposes. */
    public Model(int[][] rawValues, int score, int maxScore, boolean gameOver) {
        int size = rawValues.length;
        board = new Board(rawValues, score);
        this.score = score;
        this.maxScore = maxScore;
        this.gameOver = gameOver;
    }

    /** Return the current Tile at (COL, ROW), where 0 <= ROW < size(),
     *  0 <= COL < size(). Returns null if there is no tile there.
     *  Used for testing. Should be deprecated and removed.
     *  */
    public Tile tile(int col, int row) {
        return board.tile(col, row);
    }

    /** Return the number of squares on one side of the board.
     *  Used for testing. Should be deprecated and removed. */
    public int size() {
        return board.size();
    }

    /** Return true iff the game is over (there are no moves, or
     *  there is a tile with value 2048 on the board). */
    public boolean gameOver() {
        checkGameOver();
        if (gameOver) {
            maxScore = Math.max(score, maxScore);
        }
        return gameOver;
    }

    /** Return the current score. */
    public int score() {
        return score;
    }

    /** Return the current maximum game score (updated at end of game). */
    public int maxScore() {
        return maxScore;
    }

    /** Clear the board to empty and reset the score. */
    public void clear() {
        score = 0;
        gameOver = false;
        board.clear();
        setChanged();
    }

    /** Add TILE to the board. There must be no Tile currently at the
     *  same position. */
    public void addTile(Tile tile) {
        board.addTile(tile);
        checkGameOver();
        setChanged();
    }
    /** a function to count the number of up steps each tile needs to make since board.move can only be called once
     * it has a range (0-3) */
    private int countSteps(int col, int row, boolean[] canMerge){
        int count  = 0;
        for (int i = row + 1; i < board.size(); i++){
            if (board.tile(col,i) == null){
                count++;
            } else{
                if(board.tile(col,i).value() == board.tile(col,row).value() && canMerge[i] == true){
                    count++;
                }
                break;
            }
        }
        return count;
    }


    /** a fuction to update each col after tilting North. it moves each tile up and merges adjacent and equal valued tiles
     *  the fuction returns true if any change was made*/

    private boolean updateCol(int col,boolean changed){
        int upSteps = 0;
        boolean[] canMerge = new boolean[board.size()]; // to prevent merging twice 2 2 * 4 => 4 4 * *  (not 8 * * *)
        java.util.Arrays.fill(canMerge, Boolean.TRUE);
        for (int row = 2; row >- 1; row--){
            if (board.tile(col,row) != null){
                Tile t = board.tile(col,row);
                upSteps = countSteps(col,row,canMerge);
                if (upSteps == 0){
                    continue;
                }
                if (board.tile(col, row+upSteps) != null ){ // since the new tile is not empty then
                    score += 2* t.value();                      // both tiles have the same value (checked in countSteps())
                    canMerge[row+upSteps] = false;
                }
                board.move(col,row+upSteps,t);
                changed = true;

            }
        }
        return changed;
    }

    /** Tilt the board toward SIDE. Return true iff this changes the board.
     *
     * 1. If two Tile objects are adjacent in the direction of motion and have
     *    the same value, they are merged into one Tile of twice the original
     *    value and that new value is added to the score instance variable
     * 2. A tile that is the result of a merge will not merge again on that
     *    tilt. So each move, every tile will only ever be part of at most one
     *    merge (perhaps zero).
     * 3. When three adjacent tiles in the direction of motion have the same
     *    value, then the leading two tiles in the direction of motion merge,
     *    and the trailing tile does not.
     * */
    public boolean tilt(Side side) {
        boolean changed;
        changed = false;

        board.setViewingPerspective(side); // rotate the board to make the current side act as North

        for (int col = 0; col < board.size(); col++) {
            changed = updateCol(col,changed);
        }

        board.setViewingPerspective(Side.NORTH);  // set the North side as the true North
        checkGameOver();

        if (changed) {
            setChanged();
        }
        return changed;
    }

    /** Checks if the game is over and sets the gameOver variable
     *  appropriately.
     */
    private void checkGameOver() {
        gameOver = checkGameOver(board);
    }

    /** Determine whether game is over. */
    private static boolean checkGameOver(Board b) {
        return maxTileExists(b) || !atLeastOneMoveExists(b);
    }

    /** Returns true if at least one space on the Board is empty.
     *  Empty spaces are stored as null.
     * */
    public static boolean emptySpaceExists(Board b) {

        int col , row ;
        for (row = 0 ; row < b.size() ; row++ ){
            for (col = 0; col < b.size(); col++){
                if (b.tile(col,row) == null)
                    return true;
            }
        }
        return false;
    }

    /**
     * Returns true if any tile is equal to the maximum valid value.
     * Maximum valid value is given by MAX_PIECE. Note that
     * given a Tile object t, we get its value with t.value().
     */
    public static boolean maxTileExists(Board b) {
        int col , row ;
        for (row = 0 ; row < b.size() ; row++ ){
            for (col = 0; col < b.size(); col++){
                if (b.tile(col,row) == null){
                    continue;
                }
                if (b.tile(col,row).value() == MAX_PIECE)
                    return true;
            }
        }
        return false;
    }

    /**
     * Returns true if there are any valid moves on the board.
     * 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.
     */
    public static boolean atLeastOneMoveExists(Board b) {
        int row, col;
        if (emptySpaceExists(b) == true){
            return true;
        }
        for (row = 0 ; row < b.size() ; row++ ){
            for (col = 0; col < b.size() -1; col++) {
                if (b.tile(col, row).value() == b.tile(col + 1, row).value()) {
                    return true;
                }
            }
        }
        for (col = 0 ; col < b.size() ; col++ ) {
            for (row = 0; row < b.size() - 1; row++) {
                if (b.tile(col, row).value() == b.tile(col, row + 1).value()) {
                    return true;
                }
            }
        }
        return false;
    }


    @Override
     /** Returns the model as a string, used for debugging. */
    public String toString() {
        Formatter out = new Formatter();
        out.format("%n[%n");
        for (int row = size() - 1; row >= 0; row -= 1) {
            for (int col = 0; col < size(); col += 1) {
                if (tile(col, row) == null) {
                    out.format("|    ");
                } else {
                    out.format("|%4d", tile(col, row).value());
                }
            }
            out.format("|%n");
        }
        String over = gameOver() ? "over" : "not over";
        out.format("] %d (max: %d) (game is %s) %n", score(), maxScore(), over);
        return out.toString();
    }

    @Override
    /** Returns whether two models are equal. */
    public boolean equals(Object o) {
        if (o == null) {
            return false;
        } else if (getClass() != o.getClass()) {
            return false;
        } else {
            return toString().equals(o.toString());
        }
    }

    @Override
    /** Returns hash code of Model’s string. */
    public int hashCode() {
        return toString().hashCode();
    }
}