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(); } }