Four in a row, one click at a time

A two-player Connect Four game written in Java Swing, built on the Model-View-Controller and Observer patterns. This page follows a single click, the winning move on the board here, through every class it touches.

  • 6 × 7board
  • 4in a row wins
  • 69lines checked per move, at most
  • 2observer interfaces
Player 1 (red) drops a chip into column 4 and completes a diagonal. As in the code, rows and columns count from 0 and row 0 is the top.
  1. Click Drop

    View

    ConnectFourView is a JFrame with three panels: a turn label at the top, a grid in the middle with a Drop button above each column and a JPanel for each of the 42 slots, and Exit and Reset at the bottom. Every button reports to the same actionPerformed method.

    The method compares the event's source with each button. Exit and Reset map straight to the controller; anything else must be a Drop button, so it loops over the seven of them to find the column.

    public void actionPerformed(ActionEvent e) {
        if (e.getSource() == exit) {
            controller.exit();
        } else if (e.getSource() == reset) {
            controller.reset();
        } else {
            controller.dropChip(getDropButtonPressed(e));
        }
    }

    Clicking the fifth Drop button calls controller.dropChip(4).

  2. The controller relays it

    Controller

    ConnectFourController knows nothing about the rules. It hands the column to the model, then asks the view to update the turn label.

    public void dropChip(int column) {
        model.dropChip(column);
        view.switchTurns();
    }

    The view only sees ConnectFourControllerInterface and the controller only sees ConnectFourModelInterface, so each class can be tested with Mockito mocks standing in for the others.

    Exit and Reset take the same route: exit() calls model.exit(), which ends the program with System.exit(0), and reset() calls model.reset() and then view.resetView().

  3. The model places the chip

    Model

    ConnectFourModel stores the board as a 6 × 7 int array: 0 for an empty slot, 1 for Player 1 and 2 for Player 2. A second array, availableSpot, holds the lowest free row in each column, so dropping a chip is a lookup rather than a search.

    The board as the model sees it

    After the move, board[2][4] holds 1 and availableSpot[4] has moved up from 2 to 1.

    board[availableSpot[column]][column] = currentPlayer.getId();
    currentRow = availableSpot[column];
    currentColumn = column;
    availableSpot[column]--;
    if (availableSpot[column] < 0) {
        disablingButton = column;
    }

    When a column fills up

    Every column starts with availableSpot at 5, the bottom row. Each chip moves it up one row. Once it drops below 0 the column is full, and the model records the column in disablingButton so the view can switch its Drop button off.

    Chips dropped into one column and the availableSpot counter after each
    ChipLands in rowavailableSpot afterDrop button
    1st54Enabled
    2nd43Enabled
    3rd32Enabled
    4th21Enabled
    5th10Enabled
    6th0−1Disabled
  4. Observers repaint the window

    ModelView

    The model never calls the view directly. It loops over its registered ConnectFourObservers and calls updateGame() on each. The view registered itself in its constructor, so it receives the call and asks the model what changed.

    The calls so far

    1. View→ControllerdropChip(4)
    2. Controller→ModeldropChip(4)
    3. Modelboard[2][4] = 1, availableSpot[4] = 1
    4. Model→ViewupdateGame() through ConnectFourObserver
    5. View→ModelgetCurrentRow() → 2, getCurrentColumn() → 4, getColor() → red
    6. ViewdropChipInView(2, 4, RED) paints the slot
    7. View→ModelgetDisableButton() → −1, so every Drop button stays on

    getColor() returns the current player's color. It is still Player 1's turn here, because the model switches players only after the observers have run.

  5. Scan for four in a row

    Model

    Next, checkWin() looks across the whole board, not just around the new chip. Each check takes a starting slot that isn't empty and compares it with the next three slots in one direction. On a 6 × 7 board that is at most 69 windows, which is cheap enough not to need anything smarter.

    Horizontal24 windows
    Vertical21 windows
    Left diagonal /12 windows
    Right diagonal \12 windows

    For this move, all 24 horizontal and 21 vertical windows come up empty. The 10th left-diagonal window, starting at row 2, column 4, holds four 1s, so the win is found on window 55.

    checkWin() joins the four scans with ||, so it stops at the first line it finds. If nobody has won, boardFull() checks all 42 slots for a 0; if none is left, the game is a draw.

  6. Announce the result and reset

    ModelViewController

    The model shows a dialog, Player 1 wins! or The game ended as draw!, and waits for OK. Then it notifies its WinnerObservers. The view reacts to updateWinner() by asking the controller to reset, which clears both the model and the window.

    The rest of the click

    1. ModelJOptionPane.showMessageDialog(…) shows Player 1 wins!
    2. Model→ViewupdateWinner() through WinnerObserver
    3. View→Controllerreset()
    4. Controller→Modelreset() clears the board, refills availableSpot with 5 and makes Player 1 current
    5. Controller→ViewresetView() clears every slot, re-enables every Drop button and updates the label
    6. ModelswitchPlayer() twice, back in dropChip
    7. Controller→ViewswitchTurns() sets the label to Turn: Player 1

    Why Player 1 always starts the next game

    Every move ends with a player switch. A finished game adds one more, which cancels it out after the reset.

    The current player through the end of a game
    MomentCurrent player
    Reset during updateWinner()Player 1
    Extra switch after a win or drawPlayer 2
    Switch at the end of every movePlayer 1

How it's put together

Interfaces between the layers

The view only knows the controller's interface, the controller only knows the model's interface, and the model only knows its two observer interfaces. The one concrete link is the ConnectFourView that the controller creates.

Tested layer by layer

More than 100 JUnit 5 tests use Mockito to isolate each class. Win tests intercept the result dialog with MockedStatic, and the Swing tests run on the Event Dispatch Thread.

Checked on every change

GitHub Actions builds every push and pull request, running Checkstyle (Google Java Style) and the full test suite under a virtual display. Tagging a version publishes a release with the JAR.

Run it yourself

You need Java 21 or newer.

Download the game

Download connect-four.jar

Built from the latest main branch when this page was published. Run it with:

java -jar connect-four.jar

Build from source

The Gradle wrapper downloads everything else it needs. On Windows, use gradlew.bat.

git clone https://github.com/NiharParikh17/Connect-Four.git
cd Connect-Four
./gradlew run