package cs3500.connectn.model; import java.util.*; import static cs3500.connectn.model.Position.posn; /** * An abstract Connect N game implementing the {@link Model} interface. * *

* Impl. Note. Class invariants include: * *

*/ final class ModelImpl implements Model { /** The width of the grid. */ private final int width; /** The height of the grid. */ private final int height; /** The winning line length for the game. */ private final int goal; /** The set of winning positions forming a line, once the game is won. */ private Set winningPositions; /** The current status of the game. */ private Status status; /** The player to play next. */ private Player turn; // if the status is Won then turn is the winner /** The number of moves remaining until stalemate. */ private int movesRemaining; /** The grid of player tokens, column-major. */ private final List> columns; /** * Constructs a new game model with the given configuration and list of * player names. This is an internal, unchecked API, so satisfying the * preconditions is the responsibility of the caller. * * @param w the width (positive, unchecked) * @param h the height (positive, unchecked) * @param g the goal line length (> 1, unchecked) */ private ModelImpl(int w, int h, int g) { width = w; height = h; goal = g; status = Status.Playing; turn = Player.White; movesRemaining = width * height; columns = new ArrayList<>(width); for (int i = 0; i < width; ++i) { columns.add(new ArrayList<>(height)); } } @Override public int getWidth() { return width; } @Override public int getHeight() { return height; } @Override public int getGoal() { return goal; } @Override public Status getStatus() { return status; } @Override public Player getNextPlayer() { if (isGameOver()) { throw new IllegalStateException("the game is over"); } return turn; } @Override public Player getWinner() { if (getStatus() != Status.Won) { throw new IllegalStateException("the game isn't over"); } return turn; } @Override public Set getWinningPositions() { if (getStatus() != Status.Won) { throw new IllegalStateException("the game isn't over"); } return winningPositions; } @Override public Player getPlayerAt(int x, int y) { ensureBounds(x, y); List column = columns.get(x); if (y < column.size()) { return column.get(y); } else { return null; } } @Override public int getColumnSize(int which) { // The call to get() has the same precondition on {@code which} as this // method does, so it will throw IndexOutOfBoundsException when we need // it to. return columns.get(which).size(); } @Override public int move(Player who, int col) throws IllegalMoveException { if (isGameOver()) { throw new IllegalStateException("Move after game over"); } if (who != turn) { throw new IllegalStateException("Move out of turn"); } if (!isColumnInBounds(col)) { throw new ColumnOutOfBoundsException(getWidth() - 1); } if (isColumnFull(col)) { throw new ColumnFullException(col); } /* The column list is only long enough to contain the tokens played * thus far, so it's possible that a valid row is above the filled * portion of the column. */ List column = columns.get(col); int row = column.size(); column.add(who); --movesRemaining; checkForGameOver(col, row); if (!isGameOver()) { turn = turn.other(); } return row; } /** * Checks for game-over, either a win or a stalemate, and updates the state * accordingly. It takes the coordinates of the most recent move in order to * look for newly completed lines involving that position, rather than looking * everywhere. * * @param col the column of the most recent move * @param row the row of the most recent move */ private void checkForGameOver(int col, int row) { // First check for a winner by looking for lines in each direction. for (Position posn : DIRECTIONS) { // We count in the forward direction using the pair of offsets and the // backward direction by using their opposites. Set positions = new HashSet<>(); findLineInDirection(positions, col, row, posn.x(), posn.y()); findLineInDirection(positions, col, row, -posn.x(), -posn.y()); // Counting both directions and the current move, if we find a line // longer than goal then the game is won! if (positions.size() >= goal) { status = Status.Won; winningPositions = positions; return; } } // Sets the status to stalemate if the grid is full with no winner: if (0 == movesRemaining) { status = Status.Stalemate; } } /** * Offsets specifying the directions in which lines may be made. These are * used to search for winning lines in each of four directions as follows. */ private static Position[] DIRECTIONS = new Position[] { posn(0, 1), // Up and down posn(1, 0), // Left and right posn(1, 1), // Up-right and down-left posn(1, -1), // Up-left and down-right }; /** * Finds the longest line starting at the given point and extending * in the given direction. All {@code Position}s in the line are added to * the given set {@code acc}. The direction is specified in terms of x and * y offsets, which is sufficient to expression the eight relevant * directions on a Connect N grid. * * @param acc set to add * @param x column to start at * @param y row to start at * @param dx step in the x direction * @param dy step in the y direction */ private void findLineInDirection(Set acc, int x, int y, int dx, int dy) { Player player = getPlayerAt(x, y); // There should always be a token at (x, y) when this method is called assert player != null; while (areInBounds(x, y) // It's important that player is the receiver here, since // getPlayerAt may return null: && player.equals(getPlayerAt(x, y))) { acc.add(posn(x, y)); x += dx; y += dy; } } /** * Ensures that the coordinates are in bounds for this game by throwing an * exception if they aren't. * * @param x the column, 0-based from left * @param y the row, 0-based from bottom * @throws IndexOutOfBoundsException if (x, y) are out of bounds */ private void ensureBounds(int x, int y) { if (!areInBounds(x, y)) { throw new IndexOutOfBoundsException("coordinates are out of bounds"); } } /** * Checks whether the coordinates are in bounds for this game. * * @param x the column, 0-based from left * @param y the row, 0-based from bottom * @return whether (x, y) are in bounds */ private boolean areInBounds(int x, int y) { return isColumnInBounds(x) && rowIsInBounds(y); } private boolean rowIsInBounds(int y) { return y >= 0 && y < getHeight(); } private boolean isColumnInBounds(int x) { return x >= 0 && x < getWidth(); } /** * Configures and builds a {@link Model} in builder-pattern style. This * allows the client to configure several parameters as needed, each * clearly identified by name rather than mere position. */ protected static final class BuilderImpl implements Builder { // Typically a simple builder has a field for each parameter that needs // to be configured for the object to be built. private int width = DEFAULT_WIDTH; private int height = DEFAULT_HEIGHT; private int goal = DEFAULT_GOAL; // Invariants are the same as for ModelImpl: // // - width > 0 // - height > 0 // - goal > 1 @Override public Builder width(int width) { // Builder setters should check preconditions (that is, checks on // arguments for the purpose of preserving invariants), so that the // state of the builder is always valid and the build() method doesn't // do validation, which might fail. if (width < 1) { throw new IllegalArgumentException("dimensions must be positive"); } this.width = width; // Builders typically return {@code this} so that they can be used in // chaining style: b.width(w).height(h).build() return this; } @Override public Builder height(int height) { if (height < 1) { throw new IllegalArgumentException("dimensions must be positive"); } this.height = height; return this; } @Override public Builder goal(int goal) { if (goal < 2) { throw new IllegalArgumentException("N must be at least 2"); } this.goal = goal; return this; } @Override public Model build() { // All argument checking has already been done in the setters. return new ModelImpl(width, height, goal); } } }