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:
*
*
*
* - {@code width > 0}
*
*
- {@code height > 0}
*
*
- {@code goal > 1}
*
*
- {@code columns.size() == width}
*
*
- for every {@code column} in {@code columns},
*
*
* - {@code column.size() <= height}
* - for every {@code player} in {@code column}, {@code player != null}
*
*
* - {@code status != null}
*
*
*
* - if {@code status == Playing}, then {@code winningPositions == null}
* and
* the grid isn't full
*
*
- if {@code status == Stalemate}, then {@code winningPositions ==
* null} and
* the grid *is* full
*
*
- if {@code status == Won}, then {@code winningPositions.size() >=
* goal}, the
* positions form a line, and that line belongs to player {@code turn}
*
*
*
* - {@code turn != null}
*
*
*/
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);
}
}
}