I saw some approaches to the Connect Four game on a site. None of them seemed very OO like, therefore I'd like to present my implementation and request suggestions on how to make this code more readable.
For example, I dislike the "oddball solution" where I get the current positions stone from the field in class ConnectFourOo
(playerAt()
) because the setPlayer()
method is in class Position
. It feels more natural to have it there since then I'd reduce the places where the meaning of x
and y
could be mixed up. But when I move playerAt()
to class Position
then I find it hard to come up with a good name for it. I'd like to get any comments or suggestions on that and of course anything else which is doubtful in this code.
import java.util.Arrays;
import java.util.Scanner;
public class ConnectFourOo {
private static final int COLUMN_COUNT = 7;
private static final int ROW_COUNT = 5;
private static final int TOP_ROW_INDEX = 0;
private static final int REQUIRED_CONSECUTIVE_FIELDS = 4;
private final Stone[][] field;
private Scanner scanner;
private Stone currentPlayer;
private Boundary boundary;
public ConnectFourOo(Boundary boundary) {
this.boundary = boundary;
scanner = new Scanner(System.in);
// Player NONE transfers to player 1 before first input
currentPlayer = Stone.NONE;
field = new Stone[boundary.upper.y][boundary.upper.x];
// initialize array so we don't have to deal with NULLs
for (Player[] row : field) {
Arrays.fill(row, Stone.NONE);
}
}
public static void main(String[] args) {
new ConnectFourOo(new Boundary(COLUMN_COUNT, ROW_COUNT)).play();
}
private void play() {
Player winner;
do {
currentPlayer = currentPlayer.next();
winner = nextMove();
} while (Stone.NONE == winner);
showGame();
System.out.println("the winner is " + winner.name());
}
private Player nextMove() {
showGame();
int playersColumn = getUserInput();
Position pos = putAtColumn(playersColumn);
return checkWin(pos);
}
private int getUserInput() {
System.out.print(String.format("%s, please select a column (1 - %d):", currentPlayer.name(), boundary.upper.x));
int playersColumn = scanner.nextInt() - 1;
return playersColumn;
}
private Player checkWin(Position lastMove) {
// before this move there were no 4 connected stones on the board so we
// only need to check if this stone completes a line of four in any direction
for (Direction direction : Direction.values()) {
Position lastConnected = lastConnectedOwnFieldInOpposite(direction, lastMove);
int samePlayerCount = countCosecutiveStonesOfCurrentPlayerIn(direction, lastConnected);
if (REQUIRED_CONSECUTIVE_FIELDS == samePlayerCount)
return currentPlayer;
}
return Stone.NONE;
}
private Position lastConnectedOwnFieldInOpposite(Direction direction, Position lastMove) {
Position next = lastMove;
Position pos;
do {
pos = next;
next = direction.reverseNext(pos);
} while (next.isInside(boundary) && currentPlayer == playerAt(next));
return pos;
}
private int countCosecutiveStonesOfCurrentPlayerIn(Direction direction, Position lastMove) {
int samePlayerCount = 1; // current stone belongs to current player
// already
Position positionToCheck = lastMove;
for (int i = samePlayerCount; i < REQUIRED_CONSECUTIVE_FIELDS; i++) {
positionToCheck = direction.next(positionToCheck);
if (positionToCheck.isInside(boundary) && playerAt(positionToCheck) == currentPlayer)
samePlayerCount++;
}
return samePlayerCount;
}
private Player playerAt(Position positionToCheck) {
return field[positionToCheck.y][positionToCheck.x];
}
private Position putAtColumn(int selectedColumn) {
Position next = new Position(selectedColumn, TOP_ROW_INDEX);
Position pos;
do {
pos = next;
next = Direction.DOWN.next(pos);
} while (next.isInside(boundary) && Stone.NONE == playerAt(next));
pos.setPlayer(currentPlayer, field);
return pos;
}
private void showGame() {
System.out.println();
for (Stone[] row : field) {
for (Stone field : row) {
System.out.print(field.toString());
}
System.out.println();
}
for (int i = 0; i < boundary.upper.x; i++) {
System.out.print(String.format(" %d ", (1 + i)));
}
System.out.println();
}
}
public interface Player {
abstract Player next();
abstract String name();
}
enum Stone implements Player {
NONE(" ") {
@Override
public Stone next() {
return PLAYER_1;
}
},
PLAYER_1("○") {
@Override
public Stone next() {
return PLAYER_2;
}
},
PLAYER_2("●") {
@Override
public Stone next() {
return PLAYER_1;
}
};
private final String image;
Stone(String image) {
this.image = image;
}
public String toString() {
return String.format("[%s]", image);
}
}
class Position {
final int x, y;
public Position(int x, int y) {
this.x = x;
this.y = y;
}
boolean isInside(Boundary boundary) {
return x > boundary.lower.x && y > boundary.lower.y && x < boundary.upper.x && y < boundary.upper.y;
}
void setPlayer(Player thePlayer, Stone[][] field) {
field[y][x] = (Stone) thePlayer;
}
}
enum Direction {
// Only half of the directions needed since we first find the last connected
// stone in the opposite direction
LEFT {
@Override
Position next(Position pos) {
return new Position(pos.x - 1, pos.y);
}
},
DOWN {
@Override
Position next(Position pos) {
return new Position(pos.x, pos.y + 1);
}
},
DOWN_LEFT {
@Override
Position next(Position pos) {
return new Position(pos.x - 1, pos.y + 1);
}
},
DOWN_RIGHT {
@Override
Position next(Position pos) {
return new Position(pos.x + 1, pos.y + 1);
}
};
abstract Position next(Position pos);
public Position reverseNext(Position pos) {
Position next = next(pos);
int reverseX = 2 * pos.x - next.x;
int reverseY = 2 * pos.y - next.y;
return new Position(reverseX, reverseY);
}
}