1. Introduction
In this article, we’re going to see how we can implement the game Connect 4 in Java. We’ll see what the game looks like and how it plays and then look into how we can implement those rules.
2. What Is Connect 4?
Before we can implement our game, we need to understand the rules of the game.
Connect 4 is a relatively simple game. Players take turns dropping pieces onto the top of one of a set of piles. After each turn, if any player’s pieces make a line of four in any straight-line direction – horizontal, vertical, or diagonal – then that player is the winner:
If not, the next player gets to go instead. This then repeats until either one player wins or the game is unwinnable.
Notably, players get free choice of which column to place their piece, but that piece must go on the top of the pile. They don’t get a free choice of which row within the column their piece goes.
To build this as a computer game, we need to consider several different components: the game board itself, the ability for a player to place a token, and the ability to check if the game has been won. We’ll look at each of these in turn.
3. Defining the Game Board
Before we can play our game, we first need somewhere to play. This is the game board, which contains all the cells that players can play into and indicates where players have already placed their pieces.
We’ll start by writing an enumeration that represents the pieces that players can use in the game:
public enum Piece {
PLAYER_1,
PLAYER_2
}
This assumes that there are only two players in the game, which is typical for Connect 4.
Now, we’ll create a class that represents the game board:
public class GameBoard {
private final List<List<Piece>> columns;
private final int rows;
public GameBoard(int columns, int rows) {
this.rows = rows;
this.columns = new ArrayList<>();
for (int i = 0; i < columns; ++i) {
this.columns.add(new ArrayList<>());
}
}
public int getRows() {
return rows;
}
public int getColumns() {
return columns.size();
}
}
Here, we’re representing the game board with a list of lists. Each of these lists represents an entire column in the game, and each entry in a list represents a piece within that column.
Pieces must be stacked from the bottom, so we don’t need to account for gaps. Instead, all the gaps are at the top of the column above the inserted pieces. As such, we’re actually storing the pieces in the order they were added to the column.
Next, we’ll add a helper to get the piece that’s currently in any given cell on the board:
public Piece getCell(int x, int y) {
assert(x >= 0 && x < getColumns());
assert(y >= 0 && y < getRows());
List<Piece> column = columns.get(x);
if (column.size() > y) {
return column.get(y);
} else {
return null;
}
}
This takes an X-ordinate that starts from the first column and a Y-ordinate that starts from the bottom row. We’ll then return the correct Piece for that cell or null if there’s nothing in that cell yet.
4. Playing Moves
Now that we’ve got a game board, we need to be able to play moves on it. A player moves by adding their piece to the top of a given column. As such, we can do this by just adding a new method that takes the column and the player who’s making the move:
public void move(int x, Piece player) {
assert(x >= 0 && x < getColumns());
List<Piece> column = columns.get(x);
if (column.size() >= this.rows) {
throw new IllegalArgumentException("That column is full");
}
column.add(player);
}
We’ve also added an extra check in here. If the column in question already has too many pieces in it, then this will throw an exception instead of allowing the player to move.
5. Checking for Winning Conditions
Once a player has moved, the next step is to check if they’ve won. This means looking for anywhere on the board that we have four pieces from the same player in a horizontal, vertical, or diagonal line.
However, we can do better than this. There are certain facts that we know from how the game plays that allow us to streamline the search.
Firstly, because the game ends when a winning move is played, only the player who’s just moved can win. This means we only need to check for lines of that player’s pieces.
Secondly, the winning line must contain the piece that’s just been placed. This means we don’t need to search the entire board but only the subset that contains the played piece.
Thirdly, we can ignore certain impossible cases because of the column nature of the game. For example, we can only have a vertical line if the newest piece is on at least row 4. Anything below that, and there can’t be four in a line.
Ultimately, this means that we have the following sets to search for:
- A single vertical line starting from the newest piece and going down three rows
- One of four possible horizontal lines – the first of these starts three columns to the left and ends on our newest piece, while the last of these starts on our newest piece and ends three columns to the right
- One of four possible leading diagonal lines – the first of these starts with three columns to the left and three rows above our newest piece, while the last of these starts on our newest piece and ends three columns to the right and three rows below
- One of four possible trailing diagonal lines – the first of these starts with three columns to the left and three rows below our newest piece, while the last of these starts on our newest piece and ends three columns to the right and three rows above
This means that after each move, we must check a maximum of 13 possible lines – and some of those may be impossible given the size of the board:
Here, for example, we can see there are a few lines that fall outside the play area and, thus, can’t ever be winning lines.
5.1. Checking for a Winning Line
The first thing we need is a method to check a given line. This will take the starting point and the direction of the line and check if every cell on that line is for the current player:
private boolean checkLine(int x1, int y1, int xDiff, int yDiff, Piece player) {
for (int i = 0; i < 4; ++i) {
int x = x1 + (xDiff * i);
int y = y1 + (yDiff * i);
if (x < 0 || x > columns.size() - 1) {
return false;
}
if (y < 0 || y > rows - 1) {
return false;
}
if (player != getCell(x, y)) {
return false;
}
}
return true;
}
We’re also checking that the cells exist, and if we ever check one that doesn’t, we immediately return that this isn’t a winning line. We could do this before the loop, but we’re only checking four cells, and the additional complexity of working out the start and end of the line isn’t beneficial in this case.
5.2. Checking All Possible Lines
Next, we need to check all the possible lines. If any of those returns true, then we can immediately stop and declare that the player has won. After all, it doesn’t matter if they managed to get multiple winning lines in the same move:
private boolean checkWin(int x, int y, Piece player) {
// Vertical line
if (checkLine(x, y, 0, -1, player)) {
return true;
}
for (int offset = 0; offset < 4; ++offset) {
// Horizontal line
if (checkLine(x - 3 + offset, y, 1, 0, player)) {
return true;
}
// Leading diagonal
if (checkLine(x - 3 + offset, y + 3 - offset, 1, -1, player)) {
return true;
}
// Trailing diagonal
if (checkLine(x - 3 + offset, y - 3 + offset, 1, 1, player)) {
return true;
}
}
return false;
}
This works with a sliding offset going from left to right and uses that to determine where on each of our lines we’re going to start. The lines start by sliding three cells to the left because the fourth cell is the one we’re currently playing into, which must be included. The last line checked starts on the cell that’s just been played into and goes three cells to the right.
Finally, we update our move() function to check the winning status and return true or false accordingly:
public boolean move(int x, Piece player) {
// Unchanged from before.
return checkWin(x, column.size() - 1, player);
}
5.3. Playing the Game
At this point, we have a playable game. We can create a new game board and take turns placing pieces until we get a winning move:
GameBoard gameBoard = new GameBoard(8, 6);
assertFalse(gameBoard.move(3, Piece.PLAYER_1));
assertFalse(gameBoard.move(2, Piece.PLAYER_2));
assertFalse(gameBoard.move(4, Piece.PLAYER_1));
assertFalse(gameBoard.move(3, Piece.PLAYER_2));
assertFalse(gameBoard.move(5, Piece.PLAYER_1));
assertFalse(gameBoard.move(6, Piece.PLAYER_2));
assertFalse(gameBoard.move(5, Piece.PLAYER_1));
assertFalse(gameBoard.move(4, Piece.PLAYER_2));
assertFalse(gameBoard.move(5, Piece.PLAYER_1));
assertFalse(gameBoard.move(5, Piece.PLAYER_2));
assertFalse(gameBoard.move(6, Piece.PLAYER_1));
assertTrue(gameBoard.move(4, Piece.PLAYER_2));
This set of moves is precisely what we saw right at the start, and we can see how the very last activity returns that this has now won the game.
6. Conclusion
Here, we’ve seen how the Connect 4 game plays and then how to implement the rules in Java. Why not try building it yourself and making a full game out of it?
As always, the code from this article is available over on GitHub.