Implement Connect 4 Game with Java – 用 Java 实现 Connect 4 游戏

最后修改: 2023年 10月 31日

中文/混合/英文(键盘快捷键:t)

1. Introduction

1.导言

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.

在本文中,我们将了解如何用 Java 实现 Connect 4 游戏。我们将了解游戏的外观和玩法,然后研究如何实现这些规则。

2. What Is Connect 4?

2. 什么是连接 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:

Connect 4 是一款相对简单的游戏。玩家轮流将棋子扔到一组棋子堆的顶端。每轮游戏结束后,如果有玩家的棋子在水平、垂直或对角线等任何直线方向上排成一条四线,则该玩家获胜:

Connect 4

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

3.确定游戏棋盘

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.

假设游戏中只有两名玩家,这也是 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.

这需要一个从第一列开始的 X 坐标和一个从最下面一行开始的 Y 坐标。然后,我们将为该单元格返回正确的 Piecenull (如果该单元格中没有任何内容)。

4. Playing Moves

4.出招

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

5.检查获胜条件

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.

第三,由于游戏的列性质,我们可以忽略某些不可能的情况。例如,只有当最新的棋子至少位于第 4 行时,我们才能有一条竖线。如果低于第 4 行,就不可能出现四子成行的情况。

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:

这意味着在每一步棋之后,我们必须检查最多 13 条可能的棋线–考虑到棋盘的大小,其中有些可能是不可能的

Connect 4 Board Size

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

5.1.检查胜利线

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

5.2.检查所有可能的线路

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:

接下来,我们需要检查所有可能的线路。如果其中任何一条返回true,那么我们就可以立即停止并宣布该棋手获胜。毕竟,如果他们在同一步棋中获得多条获胜线,那也没有关系:

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:

最后,我们更新move()函数以检查获胜状态,并相应地返回truefalse

public boolean move(int x, Piece player) {
    // Unchanged from before.

    return checkWin(x, column.size() - 1, player);
}

5.3. Playing the Game

5.3.玩游戏

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

6.结论

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?

在这里,我们已经了解了 Connect 4 游戏的玩法,以及如何在 Java 中实现游戏规则。为什么不尝试自己构建并制作一个完整的游戏呢?

As always, the code from this article is available over on GitHub.

与往常一样,本文中的代码可在 GitHub 上获取。